diff --git a/.gitignore b/.gitignore index ae3b49af21..3a4af54b75 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,6 @@ pom.xml.versionsBackup **/.env .instance_id rebuild-docker.sh +*/.run/** .run/** .run \ No newline at end of file diff --git a/application/pom.xml b/application/pom.xml index dc9f6a6bf1..e7cedeb540 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -105,6 +105,10 @@ org.thingsboard.common stats + + org.thingsboard.common + edge-api + org.thingsboard dao diff --git a/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json new file mode 100644 index 0000000000..740d64627e --- /dev/null +++ b/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json @@ -0,0 +1,162 @@ +{ + "ruleChain": { + "additionalInfo": null, + "name": "Edge Root Rule Chain", + "type": "EDGE", + "firstRuleNodeId": null, + "root": true, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 0, + "nodes": [ + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 203, + "layoutY": 259 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 378 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "layoutX": 1134, + "layoutY": 132 + }, + "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", + "name": "Push to cloud", + "debugMode": false, + "configuration": { + "version": 0 + } + } + ], + "connections": [ + { + "fromIndex": 0, + "toIndex": 3, + "type": "Success" + }, + { + "fromIndex": 1, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 2, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 3, + "toIndex": 6, + "type": "RPC Request to Device" + }, + { + "fromIndex": 3, + "toIndex": 5, + "type": "Other" + }, + { + "fromIndex": 3, + "toIndex": 2, + "type": "Post attributes" + }, + { + "fromIndex": 3, + "toIndex": 1, + "type": "Post telemetry" + }, + { + "fromIndex": 3, + "toIndex": 4, + "type": "RPC Request from Device" + }, + { + "fromIndex": 4, + "toIndex": 7, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/edge_widgets.json b/application/src/main/data/json/system/widget_bundles/edge_widgets.json new file mode 100644 index 0000000000..8b64885d80 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -0,0 +1,25 @@ +{ + "widgetsBundle": { + "alias": "edge_widgets", + "title": "Edge widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "edges_overview", + "name": "Edge Quick Overview", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EdgeOverviewSettings\",\n \"properties\": {\n \"enableDefaultTitle\": {\n \"title\": \"Display default title\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableDefaultTitle\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"widgetStyle\":{},\"actions\":{}}" + } + } + ] +} diff --git a/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json new file mode 100644 index 0000000000..740d64627e --- /dev/null +++ b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json @@ -0,0 +1,162 @@ +{ + "ruleChain": { + "additionalInfo": null, + "name": "Edge Root Rule Chain", + "type": "EDGE", + "firstRuleNodeId": null, + "root": true, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 0, + "nodes": [ + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 203, + "layoutY": 259 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 378 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "layoutX": 1134, + "layoutY": 132 + }, + "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", + "name": "Push to cloud", + "debugMode": false, + "configuration": { + "version": 0 + } + } + ], + "connections": [ + { + "fromIndex": 0, + "toIndex": 3, + "type": "Success" + }, + { + "fromIndex": 1, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 2, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 3, + "toIndex": 6, + "type": "RPC Request to Device" + }, + { + "fromIndex": 3, + "toIndex": 5, + "type": "Other" + }, + { + "fromIndex": 3, + "toIndex": 2, + "type": "Post attributes" + }, + { + "fromIndex": 3, + "toIndex": 1, + "type": "Post telemetry" + }, + { + "fromIndex": 3, + "toIndex": 4, + "type": "RPC Request from Device" + }, + { + "fromIndex": 4, + "toIndex": 7, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index a37b1fc865..8231ca81ff 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -2,6 +2,7 @@ "ruleChain": { "additionalInfo": null, "name": "Root Rule Chain", + "type": "CORE", "firstRuleNodeId": null, "root": true, "debugMode": false, diff --git a/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql b/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql new file mode 100644 index 0000000000..326ca27715 --- /dev/null +++ b/application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql @@ -0,0 +1,87 @@ +-- +-- Copyright © 2016-2021 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid, + IN system_ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE +tenant_cursor CURSOR FOR select tenant.id as tenant_id + from tenant; + tenant_id_record uuid; + customer_id_record uuid; + tenant_ttl bigint; + customer_ttl bigint; + deleted_for_entities bigint; + tenant_ttl_ts bigint; + customer_ttl_ts bigint; +BEGIN +OPEN tenant_cursor; +FETCH tenant_cursor INTO tenant_id_record; +WHILE FOUND + LOOP + EXECUTE format( + 'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L', + tenant_id_record, 'TTL') INTO tenant_ttl; + if tenant_ttl IS NULL THEN + tenant_ttl := system_ttl; +END IF; + IF tenant_ttl > 0 THEN + tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint; + deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts); + deleted := deleted + deleted_for_entities; + RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record; + deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts); + deleted := deleted + deleted_for_entities; + RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record; +END IF; +FOR customer_id_record IN +SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record + LOOP + EXECUTE format( + 'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L', + customer_id_record, 'TTL') INTO customer_ttl; +IF customer_ttl IS NULL THEN + customer_ttl_ts := tenant_ttl_ts; +ELSE + IF customer_ttl > 0 THEN + customer_ttl_ts := + (EXTRACT(EPOCH FROM current_timestamp) * 1000 - + customer_ttl::bigint * 1000)::bigint; +END IF; +END IF; + IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN + deleted_for_entities := + delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record, + customer_ttl_ts); + deleted := deleted + deleted_for_entities; + RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record; + deleted_for_entities := + delete_device_records_from_ts_kv(tenant_id_record, customer_id_record, + customer_ttl_ts); + deleted := deleted + deleted_for_entities; + RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record; + deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, + customer_id_record, + customer_ttl_ts); + deleted := deleted + deleted_for_entities; + RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record; +END IF; +END LOOP; +FETCH tenant_cursor INTO tenant_id_record; +END LOOP; +END +$$; diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql new file mode 100644 index 0000000000..b1d18c6c9b --- /dev/null +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -0,0 +1,47 @@ +-- +-- Copyright © 2016-2021 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) +); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +); diff --git a/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql new file mode 100644 index 0000000000..f70c65cbdc --- /dev/null +++ b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql @@ -0,0 +1,32 @@ +-- +-- Copyright © 2016-2021 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE OR REPLACE PROCEDURE cleanup_edge_events_by_ttl(IN ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE + ttl_ts bigint; + ttl_deleted_count bigint DEFAULT 0; +BEGIN + IF ttl > 0 THEN + ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint; + EXECUTE format( + 'WITH deleted AS (DELETE FROM edge_event WHERE ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', ttl_ts) into ttl_deleted_count; + END IF; + RAISE NOTICE 'Edge events removed by ttl: %', ttl_deleted_count; + deleted := ttl_deleted_count; +END +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 430819391a..c54f940c38 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -54,6 +55,8 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; @@ -69,7 +72,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; @@ -296,6 +299,18 @@ public class ActorSystemContext { @Getter private TbCoreDeviceRpcService tbCoreDeviceRpcService; + @Lazy + @Autowired(required = false) + @Getter private EdgeService edgeService; + + @Lazy + @Autowired(required = false) + @Getter private EdgeEventService edgeEventService; + + @Lazy + @Autowired(required = false) + @Getter private EdgeRpcService edgeRpcService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private long maxConcurrentSessionsPerDevice; @@ -320,6 +335,9 @@ public class ActorSystemContext { @Getter private long statisticsPersistFrequency; + @Value("${edges.enabled}") + @Getter + private boolean edgesEnabled; @Scheduled(fixedDelayString = "${actors.statistics.js_print_interval_ms}") public void printStats() { diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index f1248a3aad..81e9c8917a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -35,10 +35,12 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -89,10 +91,15 @@ public class AppActor extends ContextAwareActor { case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG: + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((TenantAwareMsg) msg, true); break; + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: + onToTenantActorMsg((EdgeEventUpdateMsg) msg); + break; default: return false; } @@ -192,6 +199,20 @@ public class AppActor extends ContextAwareActor { () -> new TenantActor.ActorCreator(systemContext, tenantId)); } + private void onToTenantActorMsg(EdgeEventUpdateMsg msg) { + TbActorRef target = null; + if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) { + log.warn("Message has system tenant id: {}", msg); + } else { + target = getOrCreateTenantActor(msg.getTenantId()); + } + if (target != null) { + target.tellWithHighPriority(msg); + } else { + log.debug("[{}] Invalid edge event update msg: {}", msg.getTenantId(), msg); + } + } + public static class ActorCreator extends ContextBasedCreator { public ActorCreator(ActorSystemContext context) { diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index 9233f4a317..6e2761c9c5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -17,6 +17,7 @@ package org.thingsboard.server.actors.device; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -70,12 +72,18 @@ public class DeviceActor extends ContextAwareActor { case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: processor.processRpcRequest(ctx, (ToDeviceRpcRequestActorMsg) msg); break; + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: + processor.processRpcResponsesFromEdge(ctx, (FromDeviceRpcResponseActorMsg) msg); + break; case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG: processor.processServerSideRpcTimeout(ctx, (DeviceActorServerSideRpcTimeoutMsg) msg); break; case SESSION_TIMEOUT_MSG: processor.checkSessionsTimeout(); break; + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: + processor.processEdgeUpdate((DeviceEdgeUpdateMsg) msg); + break; default: return false; } 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 41073939f2..c90c47fb1c 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.actors.device; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.thingsboard.rule.engine.api.RpcError; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.actors.ActorSystemContext; @@ -31,11 +33,17 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -68,6 +76,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceAct import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -103,6 +112,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { private String deviceName; private String deviceType; private TbMsgMetaData defaultMetaData; + private EdgeId edgeId; DeviceActorMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) { super(systemContext); @@ -125,12 +135,32 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { this.defaultMetaData = new TbMsgMetaData(); this.defaultMetaData.putValue("deviceName", deviceName); this.defaultMetaData.putValue("deviceType", deviceType); + if (systemContext.isEdgesEnabled()) { + this.edgeId = findRelatedEdgeId(); + } return true; } else { return false; } } + private EdgeId findRelatedEdgeId() { + List result = + systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.EDGE_TYPE, RelationTypeGroup.COMMON); + if (result != null && result.size() > 0) { + EntityRelation relationToEdge = result.get(0); + if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { + log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); + return new EdgeId(relationToEdge.getFrom().getId()); + } else { + log.trace("[{}][{}] edge relation is empty {}", tenantId, deviceId, relationToEdge); + } + } else { + log.trace("[{}][{}] device doesn't have any related edge", tenantId, deviceId); + } + return null; + } + void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) { ToDeviceRpcRequest request = msg.getMsg(); ToDeviceRpcRequestBody body = request.getBody(); @@ -143,15 +173,22 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { return; } - boolean sent = rpcSubscriptions.size() > 0; - Set syncSessionSet = new HashSet<>(); - rpcSubscriptions.forEach((key, value) -> { - sendToTransport(rpcRequest, key, value.getNodeId()); - if (SessionType.SYNC == value.getType()) { - syncSessionSet.add(key); - } - }); - syncSessionSet.forEach(rpcSubscriptions::remove); + boolean sent; + if (systemContext.isEdgesEnabled() && edgeId != null) { + log.debug("[{}][{}] device is related to edge [{}]. Saving RPC request to edge queue", tenantId, deviceId, edgeId.getId()); + saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()); + sent = true; + } else { + sent = rpcSubscriptions.size() > 0; + Set syncSessionSet = new HashSet<>(); + rpcSubscriptions.forEach((key, value) -> { + sendToTransport(rpcRequest, key, value.getNodeId()); + if (SessionType.SYNC == value.getType()) { + syncSessionSet.add(key); + } + }); + syncSessionSet.forEach(rpcSubscriptions::remove); + } if (request.isOneway() && sent) { log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId()); @@ -166,6 +203,17 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } } + void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) { + log.debug("[{}] Processing rpc command response from edge session", deviceId); + ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); + boolean success = requestMd != null; + if (success) { + systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(responseMsg.getMsg()); + } else { + log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId()); + } + } + private void registerPendingRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) { toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent)); DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout); @@ -498,6 +546,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { this.defaultMetaData.putValue("deviceType", deviceType); } + void processEdgeUpdate(DeviceEdgeUpdateMsg msg) { + log.trace("[{}] Processing edge update {}", deviceId, msg); + this.edgeId = msg.getEdgeId(); + } + private void sendToTransport(GetAttributeResponseMsg responseMsg, SessionInfoProto sessionInfo) { ToTransportMsg msg = ToTransportMsg.newBuilder() .setSessionIdMSB(sessionInfo.getSessionIdMSB()) @@ -530,6 +583,36 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { systemContext.getTbCoreToTransportService().process(nodeId, msg); } + private void saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(EdgeEventActionType.RPC_CALL); + edgeEvent.setEntityId(deviceId.getId()); + edgeEvent.setType(EdgeEventType.DEVICE); + + ObjectNode body = mapper.createObjectNode(); + body.put("requestId", requestId); + body.put("requestUUID", msg.getId().toString()); + body.put("oneway", msg.isOneway()); + body.put("expirationTime", msg.getExpirationTime()); + body.put("method", msg.getBody().getMethod()); + body.put("params", msg.getBody().getParams()); + edgeEvent.setBody(body); + + edgeEvent.setEdgeId(edgeId); + ListenableFuture future = systemContext.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess( EdgeEvent result) { + systemContext.getClusterService().onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, systemContext.getDbCallbackExecutor()); + } private List toTsKvProtos(@Nullable List result) { List clientAttributes; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index e1717e3b9b..ae7d8c492e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -62,6 +63,8 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -319,6 +322,11 @@ class DefaultTbContext implements TbContext { return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId); } + @Override + public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + mainCtx.getClusterService().onEdgeEventUpdate(tenantId, edgeId); + } + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { return entityActionMsg(entity, id, ruleNodeId, action, ServiceQueue.MAIN, null); } @@ -477,6 +485,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceProfileCache(); } + @Override + public EdgeService getEdgeService() { + return mainCtx.getEdgeService(); + } + + @Override + public EdgeEventService getEdgeEventService() { + return mainCtx.getEdgeEventService(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 830714b431..10d6272beb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -99,7 +100,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId); log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); // Creating and starting the actors; @@ -119,7 +120,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId); log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java index b1c6c2f0ed..76f5ec1809 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java @@ -23,13 +23,13 @@ import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate; import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.DefaultActorService; -import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.dao.rule.RuleChainService; @@ -55,7 +55,7 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { } protected void initRuleChains() { - for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChains(tenantId, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { RuleChainId ruleChainId = ruleChain.getId(); log.debug("[{}|{}] Creating rule chain actor", ruleChainId.getEntityType(), ruleChain.getId()); TbActorRef actorRef = getOrCreateActor(ruleChainId, id -> ruleChain); @@ -65,13 +65,13 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { } protected void destroyRuleChains() { - for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChains(tenantId, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { ctx.stop(new TbEntityActorId(ruleChain.getId())); } } protected void visit(RuleChain entity, TbActorRef actorRef) { - if (entity != null && entity.isRoot()) { + if (entity != null && entity.isRoot() && entity.getType().equals(RuleChainType.CORE)) { rootChain = entity; rootChainActor = actorRef; } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index d6d1d6aa9a..68afbdaf99 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -33,21 +33,27 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.List; @@ -155,13 +161,18 @@ public class TenantActor extends RuleChainManagerActor { case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG: + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((DeviceAwareMsg) msg, true); break; case RULE_CHAIN_TO_RULE_CHAIN_MSG: onRuleChainMsg((RuleChainAwareMsg) msg); break; + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: + onToEdgeSessionMsg((EdgeEventUpdateMsg) msg); + break; default: return false; } @@ -230,14 +241,26 @@ public class TenantActor extends RuleChainManagerActor { log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId); initRuleChains(); } - } - if (isRuleEngineForCurrentTenant) { + } else if (msg.getEntityId().getEntityType() == EntityType.EDGE) { + EdgeId edgeId = new EdgeId(msg.getEntityId().getId()); + EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService(); + if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { + edgeRpcService.deleteEdge(edgeId); + } else { + Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId); + if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) { + edgeRpcService.updateEdge(edge); + } + } + } else if (isRuleEngineForCurrentTenant) { TbActorRef target = getEntityActorRef(msg.getEntityId()); if (target != null) { if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { RuleChain ruleChain = systemContext.getRuleChainService(). findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); - visit(ruleChain, target); + if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { + visit(ruleChain, target); + } } target.tellWithHighPriority(msg); } else { @@ -252,6 +275,11 @@ public class TenantActor extends RuleChainManagerActor { () -> new DeviceActorCreator(systemContext, tenantId, deviceId)); } + private void onToEdgeSessionMsg(EdgeEventUpdateMsg msg) { + log.trace("[{}] onToEdgeSessionMsg [{}]", msg.getTenantId(), msg); + systemContext.getEdgeRpcService().onEdgeEvent(msg.getEdgeId()); + } + public static class ActorCreator extends ContextBasedCreator { private final TenantId tenantId; diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 8b595bd987..2718b455b9 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -71,7 +71,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; 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 454bb12213..3f32e878f4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -34,6 +34,7 @@ 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.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; @@ -93,6 +94,9 @@ public class AlarmController extends BaseController { logEntityAction(savedAlarm.getOriginator(), savedAlarm, getCurrentUser().getCustomerId(), alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + sendEntityNotificationMsg(getTenantId(), savedAlarm.getId(), alarm.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + return savedAlarm; } catch (Exception e) { logEntityAction(emptyId(EntityType.ALARM), alarm, @@ -109,8 +113,11 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); checkAlarmId(alarmId, Operation.WRITE); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.DELETED); + return alarmService.deleteAlarm(getTenantId(), alarmId); - } catch (Exception e) { + } catch (Exception e) { throw handleException(e); } } @@ -128,6 +135,8 @@ public class AlarmController extends BaseController { alarm.setAckTs(ackTs); alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_ACK); } catch (Exception e) { throw handleException(e); } @@ -146,6 +155,8 @@ public class AlarmController extends BaseController { alarm.setClearTs(clearTs); alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_CLEAR); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index fb674f894c..19280716b3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -33,14 +33,18 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -54,6 +58,8 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -98,7 +104,7 @@ public class AssetController extends BaseController { asset.setTenantId(getCurrentUser().getTenantId()); - checkEntity(asset.getId(), asset, Resource.ASSET); + checkEntity(asset.getId(), asset, Resource.ASSET); Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); @@ -106,6 +112,10 @@ public class AssetController extends BaseController { savedAsset.getCustomerId(), asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (asset.getId() != null) { + sendEntityNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED); + } + return savedAsset; } catch (Exception e) { logEntityAction(emptyId(EntityType.ASSET), asset, @@ -122,12 +132,16 @@ public class AssetController extends BaseController { try { AssetId assetId = new AssetId(toUUID(strAssetId)); Asset asset = checkAssetId(assetId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), assetId); + assetService.deleteAsset(getTenantId(), assetId); logEntityAction(assetId, asset, asset.getCustomerId(), ActionType.DELETED, null, strAssetId); + sendDeleteNotificationMsg(getTenantId(), assetId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.ASSET), null, @@ -157,6 +171,9 @@ public class AssetController extends BaseController { savedAsset.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedAsset; } catch (Exception e) { @@ -188,6 +205,9 @@ public class AssetController extends BaseController { asset.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedAsset; } catch (Exception e) { @@ -401,4 +421,113 @@ public class AssetController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST) + @ResponseBody + public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ASSET_ID, strAssetId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId, Operation.ASSIGN_TO_EDGE); + + Asset savedAsset = checkNotNull(assetService.assignAssetToEdge(getTenantId(), assetId, edgeId)); + + logEntityAction(assetId, savedAsset, + savedAsset.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strAssetId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedAsset; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.ASSET), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strAssetId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.DELETE) + @ResponseBody + public Asset unassignAssetFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ASSET_ID, strAssetId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId, Operation.UNASSIGN_FROM_EDGE); + + Asset savedAsset = checkNotNull(assetService.unassignAssetFromEdge(getTenantId(), assetId, edgeId)); + + logEntityAction(assetId, asset, + asset.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strAssetId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedAsset; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.ASSET), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strAssetId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeAssets( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredAssets = nonFilteredResult.getData().stream().filter(asset -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredAssets, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index d281de9009..f41e40ef89 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -37,6 +37,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; @@ -110,6 +111,8 @@ public class AuthController extends BaseController { userCredentials.setPassword(passwordEncoder.encode(newPassword)); userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); + sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); @@ -224,6 +227,8 @@ public class AuthController extends BaseController { } } + sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); 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 04ea69d419..8d46cce8e1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; @@ -50,6 +51,10 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; @@ -58,6 +63,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -78,7 +84,9 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -94,6 +102,7 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -110,10 +119,14 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.edge.EdgeNotificationService; +import org.thingsboard.server.service.edge.rpc.EdgeGrpcService; +import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService; import org.thingsboard.server.service.lwm2m.LwM2MModelsRepository; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; @@ -244,10 +257,25 @@ public abstract class BaseController { @Autowired protected LwM2MModelsRepository lwM2MModelsRepository; + @Autowired(required = false) + protected EdgeService edgeService; + + @Autowired(required = false) + protected EdgeNotificationService edgeNotificationService; + + @Autowired(required = false) + protected SyncEdgeService syncEdgeService; + + @Autowired(required = false) + protected EdgeGrpcService edgeGrpcService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; + @Value("${edges.enabled}") + @Getter + protected boolean edgesEnabled; @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { @@ -461,6 +489,9 @@ public abstract class BaseController { case ENTITY_VIEW: checkEntityViewId(new EntityViewId(entityId.getId()), operation); return; + case EDGE: + checkEdgeId(new EdgeId(entityId.getId()), operation); + return; case WIDGETS_BUNDLE: checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation); return; @@ -622,6 +653,30 @@ public abstract class BaseController { } } + Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException { + try { + validateId(edgeId, "Incorrect edgeId " + edgeId); + Edge edge = edgeService.findEdgeById(getTenantId(), edgeId); + checkNotNull(edge); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); + return edge; + } catch (Exception e) { + throw handleException(e, false); + } + } + + EdgeInfo checkEdgeInfoId(EdgeId edgeId, Operation operation) throws ThingsboardException { + try { + validateId(edgeId, "Incorrect edgeId " + edgeId); + EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId); + checkNotNull(edge); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); + return edge; + } catch (Exception e) { + throw handleException(e, false); + } + } + DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); @@ -643,19 +698,19 @@ public abstract class BaseController { } } - List checkComponentDescriptorsByType(ComponentType type) throws ThingsboardException { + List checkComponentDescriptorsByType(ComponentType type, RuleChainType ruleChainType) throws ThingsboardException { try { log.debug("[{}] Lookup component descriptors", type); - return componentDescriptorService.getComponents(type); + return componentDescriptorService.getComponents(type, ruleChainType); } catch (Exception e) { throw handleException(e, false); } } - List checkComponentDescriptorsByTypes(Set types) throws ThingsboardException { + List checkComponentDescriptorsByTypes(Set types, RuleChainType ruleChainType) throws ThingsboardException { try { log.debug("[{}] Lookup component descriptors", types); - return componentDescriptorService.getComponents(types); + return componentDescriptorService.getComponents(types, ruleChainType); } catch (Exception e) { throw handleException(e, false); } @@ -776,6 +831,12 @@ public abstract class BaseController { case TIMESERIES_DELETED: msgType = DataConstants.TIMESERIES_DELETED; break; + case ASSIGNED_TO_EDGE: + msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE; + break; + case UNASSIGNED_FROM_EDGE: + msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE; + break; } if (!StringUtils.isEmpty(msgType)) { try { @@ -805,6 +866,16 @@ public abstract class BaseController { String strTenantName = extractParameter(String.class, 1, additionalInfo); metaData.putValue("assignedToTenantId", strTenantId); metaData.putValue("assignedToTenantName", strTenantName); + } else if (actionType == ActionType.ASSIGNED_TO_EDGE) { + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + metaData.putValue("assignedEdgeId", strEdgeId); + metaData.putValue("assignedEdgeName", strEdgeName); + } else if (actionType == ActionType.UNASSIGNED_FROM_EDGE) { + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + metaData.putValue("unassignedEdgeId", strEdgeId); + metaData.putValue("unassignedEdgeName", strEdgeName); } ObjectNode entityNode; if (entity != null) { @@ -898,6 +969,93 @@ public abstract class BaseController { return null; } + protected void sendRelationNotificationMsg(TenantId tenantId, EntityRelation relation, EdgeEventActionType action) { + try { + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + sendNotificationMsgToEdgeService(tenantId, null, null, json.writeValueAsString(relation), EdgeEventType.RELATION, action); + } + } catch (Exception e) { + log.warn("Failed to push relation to core: {}", relation, e); + } + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, EdgeEventActionType.DELETED); + } + } + } + + protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { + try { + sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); + } catch (Exception e) { + log.warn("Failed to push assign/unassign to/from customer to core: {}", customerId, e); + } + } + + protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); + } + + protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); + } + + private void sendNotificationMsgToEdgeService(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { + if (!edgesEnabled) { + return; + } + if (type == null) { + if (entityId != null) { + type = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); + } else { + log.trace("[{}] entity id and type are null. Ignoring this notification", tenantId); + return; + } + if (type == null) { + log.trace("[{}] edge event type is null. Ignoring this notification [{}]", tenantId, entityId); + return; + } + } + TransportProtos.EdgeNotificationMsgProto.Builder builder = TransportProtos.EdgeNotificationMsgProto.newBuilder(); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.setType(type.name()); + builder.setAction(action.name()); + if (entityId != null) { + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setEntityType(entityId.getEntityType().name()); + } + if (edgeId != null) { + builder.setEdgeIdMSB(edgeId.getId().getMostSignificantBits()); + builder.setEdgeIdLSB(edgeId.getId().getLeastSignificantBits()); + } + if (body != null) { + builder.setBody(body); + } + TransportProtos.EdgeNotificationMsgProto msg = builder.build(); + log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg); + tbClusterService.pushMsgToCore(tenantId, entityId != null ? entityId : tenantId, + TransportProtos.ToCoreMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); + } + + protected List findRelatedEdgeIds(TenantId tenantId, EntityId entityId) { + if (!edgesEnabled) { + return null; + } + List result = null; + try { + result = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId).get(); + } catch (Exception e) { + log.error("[{}] can't find related edge ids for entity [{}]", tenantId, entityId, e); + } + return result; + } + private void addTimeseries(ObjectNode entityNode, List timeseries) throws Exception { if (timeseries != null && !timeseries.isEmpty()) { ArrayNode result = entityNode.putArray("timeseries"); diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 2740bf8e49..81676374f8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.HashSet; @@ -51,10 +53,11 @@ public class ComponentDescriptorController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType) throws ThingsboardException { + public List getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType, + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkParameter("componentType", strComponentType); try { - return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType)); + return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); } catch (Exception e) { throw handleException(e); } @@ -63,17 +66,28 @@ public class ComponentDescriptorController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException { + public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes, + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkArrayParameter("componentTypes", strComponentTypes); try { Set componentTypes = new HashSet<>(); for (String strComponentType : strComponentTypes) { componentTypes.add(ComponentType.valueOf(strComponentType)); } - return checkComponentDescriptorsByTypes(componentTypes); + return checkComponentDescriptorsByTypes(componentTypes, getRuleChainType(strRuleChainType)); } catch (Exception e) { throw handleException(e); } } + private RuleChainType getRuleChainType(String strRuleChainType) { + RuleChainType ruleChainType; + if (StringUtils.isEmpty(strRuleChainType)) { + ruleChainType = RuleChainType.CORE; + } else { + ruleChainType = RuleChainType.valueOf(strRuleChainType); + } + return ruleChainType; + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index dd0312169b..484b15f076 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -31,8 +31,10 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -40,6 +42,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.List; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -112,6 +116,10 @@ public class CustomerController extends BaseController { savedCustomer.getId(), customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (customer.getId() != null) { + sendEntityNotificationMsg(savedCustomer.getTenantId(), savedCustomer.getId(), EdgeEventActionType.UPDATED); + } + return savedCustomer; } catch (Exception e) { @@ -130,12 +138,16 @@ public class CustomerController extends BaseController { try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), customerId); + customerService.deleteCustomer(getTenantId(), customerId); logEntityAction(customerId, customer, customer.getId(), ActionType.DELETED, null, strCustomerId); + sendDeleteNotificationMsg(getTenantId(), customerId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.CUSTOMER), diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 2e6efc4246..8616da8714 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -38,20 +38,26 @@ import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @RestController @TbCoreComponent @@ -122,6 +128,10 @@ public class DashboardController extends BaseController { null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (dashboard.getId() != null) { + sendEntityNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), EdgeEventActionType.UPDATED); + } + return savedDashboard; } catch (Exception e) { logEntityAction(emptyId(EntityType.DASHBOARD), dashboard, @@ -139,12 +149,16 @@ public class DashboardController extends BaseController { try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), dashboardId); + dashboardService.deleteDashboard(getCurrentUser().getTenantId(), dashboardId); logEntityAction(dashboardId, dashboard, null, ActionType.DELETED, null, strDashboardId); + sendDeleteNotificationMsg(getTenantId(), dashboardId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.DASHBOARD), @@ -176,6 +190,7 @@ public class DashboardController extends BaseController { customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); return savedDashboard; } catch (Exception e) { @@ -207,6 +222,8 @@ public class DashboardController extends BaseController { customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedDashboard; } catch (Exception e) { @@ -262,6 +279,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, savedDashboard, customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); } for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); @@ -269,7 +287,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, dashboard, customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); } return savedDashboard; } @@ -313,6 +331,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, savedDashboard, customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); } return savedDashboard; } @@ -356,7 +375,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, dashboard, customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); } return savedDashboard; } @@ -578,4 +597,106 @@ public class DashboardController extends BaseController { } catch (Exception e) {} return null; } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST) + @ResponseBody + public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(DASHBOARD_ID, strDashboardId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.ASSIGN_TO_EDGE); + + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); + + logEntityAction(dashboardId, savedDashboard, + null, + ActionType.ASSIGNED_TO_EDGE, null, strDashboardId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedDashboard; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strDashboardId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) + @ResponseBody + public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(DASHBOARD_ID, strDashboardId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_EDGE); + + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); + + logEntityAction(dashboardId, dashboard, + null, + ActionType.UNASSIGNED_FROM_EDGE, null, strDashboardId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedDashboard; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strDashboardId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeDashboards( + @PathVariable("edgeId") String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredDashboards, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index c6409ebbad..9c9bfa58a1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; @@ -43,14 +44,18 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; @@ -72,6 +77,8 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -125,6 +132,10 @@ public class DeviceController extends BaseController { tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + if (device.getId() != null) { + sendEntityNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), EdgeEventActionType.UPDATED); + } + logEntityAction(savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); @@ -150,6 +161,9 @@ public class DeviceController extends BaseController { try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), deviceId); + deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId); tbClusterService.onDeviceDeleted(device, null); @@ -159,6 +173,8 @@ public class DeviceController extends BaseController { device.getCustomerId(), ActionType.DELETED, null, strDeviceId); + sendDeleteNotificationMsg(getTenantId(), deviceId, relatedEdgeIds); + deviceStateService.onDeviceDeleted(device); } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), @@ -189,6 +205,9 @@ public class DeviceController extends BaseController { savedDevice.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedDevice; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), null, @@ -217,6 +236,9 @@ public class DeviceController extends BaseController { device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedDevice; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), null, @@ -280,6 +302,9 @@ public class DeviceController extends BaseController { Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(getCurrentUser().getTenantId(), deviceCredentials)); tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId(), result), null); + + sendEntityNotificationMsg(getTenantId(), device.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + logEntityAction(device.getId(), device, device.getCustomerId(), ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); @@ -631,4 +656,115 @@ public class DeviceController extends BaseController { metaData.putValue("assignedFromTenantName", tenant.getName()); return metaData; } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.POST) + @ResponseBody + public Device assignDeviceToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(DEVICE_ID, strDeviceId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.ASSIGN_TO_EDGE); + + Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); + + tbClusterService.pushMsgToCore(new DeviceEdgeUpdateMsg(savedDevice.getTenantId(), + savedDevice.getId(), edgeId), null); + + logEntityAction(deviceId, savedDevice, + savedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strDeviceId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strDeviceId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.DELETE) + @ResponseBody + public Device unassignDeviceFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(DEVICE_ID, strDeviceId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId, Operation.UNASSIGN_FROM_EDGE); + + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); + + tbClusterService.pushMsgToCore(new DeviceEdgeUpdateMsg(savedDevice.getTenantId(), + savedDevice.getId(), null), null); + + logEntityAction(deviceId, device, + device.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strDeviceId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strDeviceId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeDevices( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredDevices = nonFilteredResult.getData().stream().filter(device -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredDevices, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 81bf0ba1a7..c02ae4646a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -153,6 +154,9 @@ public class DeviceProfileController extends BaseController { null, created ? ActionType.ADDED : ActionType.UPDATED, null); + sendEntityNotificationMsg(getTenantId(), savedDeviceProfile.getId(), + deviceProfile.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + return savedDeviceProfile; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile, @@ -178,6 +182,7 @@ public class DeviceProfileController extends BaseController { null, ActionType.DELETED, null, strDeviceProfileId); + sendEntityNotificationMsg(getTenantId(), deviceProfile.getId(), EdgeEventActionType.DELETED); } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE_PROFILE), null, diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java new file mode 100644 index 0000000000..9c196c6bd2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -0,0 +1,593 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeGrpcSession; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class EdgeController extends BaseController { + + public static final String EDGE_ID = "edgeId"; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges/enabled", method = RequestMethod.GET) + @ResponseBody + public boolean isEdgesSupportEnabled() { + return edgesEnabled; + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public Edge getEdgeById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + if (Authority.CUSTOMER_USER.equals(getCurrentUser().getAuthority())) { + cleanUpSensitiveData(edge); + } + return edge; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/info/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public EdgeInfo getEdgeInfoById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + EdgeInfo edgeInfo = checkEdgeInfoId(edgeId, Operation.READ); + if (Authority.CUSTOMER_USER.equals(getCurrentUser().getAuthority())) { + cleanUpSensitiveData(edgeInfo); + } + return edgeInfo; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge", method = RequestMethod.POST) + @ResponseBody + public Edge saveEdge(@RequestBody Edge edge) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + edge.setTenantId(tenantId); + boolean created = edge.getId() == null; + + RuleChain edgeTemplateRootRuleChain = null; + if (created) { + edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenantId); + if (edgeTemplateRootRuleChain == null) { + throw new DataValidationException("Root edge rule chain is not available!"); + } + } + + Operation operation = created ? Operation.CREATE : Operation.WRITE; + + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, + edge.getId(), edge); + + Edge savedEdge = checkNotNull(edgeService.saveEdge(edge)); + + if (created) { + ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), savedEdge.getId()); + edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + edgeService.assignDefaultRuleChainsToEdge(tenantId, savedEdge.getId()); + } + + tbClusterService.onEntityStateChange(savedEdge.getTenantId(), savedEdge.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + logEntityAction(savedEdge.getId(), savedEdge, null, created ? ActionType.ADDED : ActionType.UPDATED, null); + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), edge, + null, edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteEdge(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.DELETE); + edgeService.deleteEdge(getTenantId(), edgeId); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.DELETED); + + logEntityAction(edgeId, edge, + null, + ActionType.DELETED, null, strEdgeId); + + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.EDGE), + null, + null, + ActionType.DELETED, e, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdges(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/{customerId}/edge/{edgeId}", method = RequestMethod.POST) + @ResponseBody + public Edge assignEdgeToCustomer(@PathVariable("customerId") String strCustomerId, + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + checkParameter(EDGE_ID, strEdgeId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, customerId)); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, savedEdge, + savedEdge.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, strCustomerId, customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId, strCustomerId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/edge/{edgeId}", method = RequestMethod.DELETE) + @ResponseBody + public Edge unassignEdgeFromCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.UNASSIGN_FROM_CUSTOMER); + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Edge isn't assigned to any customer!"); + } + Customer customer = checkCustomerId(edge.getCustomerId(), Operation.READ); + + Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(getCurrentUser().getTenantId(), edgeId)); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, edge, + edge.getCustomerId(), + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEdgeId, customer.getId().toString(), customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/edge/{edgeId}", method = RequestMethod.POST) + @ResponseBody + public Edge assignEdgeToPublicCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, publicCustomer.getId())); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, savedEdge, + savedEdge.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantEdges( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantEdgeInfos( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edges", params = {"edgeName"}, method = RequestMethod.GET) + @ResponseBody + public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/{ruleChainId}/root", method = RequestMethod.POST) + @ResponseBody + public Edge setRootRuleChain(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter("ruleChainId", strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.WRITE); + + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.WRITE); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.WRITE, + edge.getId(), edge); + + Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(getTenantId(), edge, ruleChainId); + + tbClusterService.onEntityStateChange(updatedEdge.getTenantId(), updatedEdge.getId(), ComponentLifecycleEvent.UPDATED); + + logEntityAction(updatedEdge.getId(), updatedEdge, null, ActionType.UPDATED, null); + + return updatedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), + null, + null, + ActionType.UPDATED, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerEdges( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + } + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : result.getData()) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(result); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerEdgeInfos( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); + } + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : result.getData()) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(result); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET) + @ResponseBody + public List getEdgesByIds( + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException { + checkArrayParameter("edgeIds", strEdgeIds); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List edgeIds = new ArrayList<>(); + for (String strEdgeId : strEdgeIds) { + edgeIds.add(new EdgeId(toUUID(strEdgeId))); + } + ListenableFuture> edgesFuture; + if (customerId == null || customerId.isNullUid()) { + edgesFuture = edgeService.findEdgesByTenantIdAndIdsAsync(tenantId, edgeIds); + } else { + edgesFuture = edgeService.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, edgeIds); + } + List edges = edgesFuture.get(); + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : edges) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(edges); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getEdgeTypes()); + checkEntityId(query.getParameters().getEntityId(), Operation.READ); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List edges = checkNotNull(edgeService.findEdgesByQuery(tenantId, query).get()); + edges = edges.stream().filter(edge -> { + try { + accessControlService.checkPermission(user, Resource.EDGE, Operation.READ, edge.getId(), edge); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : edges) { + cleanUpSensitiveData(edge); + } + } + return edges; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/types", method = RequestMethod.GET) + @ResponseBody + public List getEdgeTypes() throws ThingsboardException { + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId); + return checkNotNull(edgeTypes.get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST) + public void syncEdge(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + if (isEdgesEnabled()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + EdgeGrpcSession session = edgeGrpcService.getEdgeGrpcSessionById(tenantId, edgeId); + Edge edge = session.getEdge(); + syncEdgeService.sync(tenantId, edge); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @RequestMapping(value = "/license/checkInstance", method = RequestMethod.POST) + @ResponseBody + public Object checkInstance(@RequestBody Object request) throws ThingsboardException { + try { + return edgeService.checkInstance(request); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } + + @RequestMapping(value = "/license/activateInstance", params = {"licenseSecret", "releaseDate"}, method = RequestMethod.POST) + @ResponseBody + public Object activateInstance(@RequestParam String licenseSecret, + @RequestParam String releaseDate) throws ThingsboardException { + try { + return edgeService.activateInstance(licenseSecret, releaseDate); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public String findMissingToRelatedRuleChains(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + return edgeService.findMissingToRelatedRuleChains(tenantId, edgeId); + } catch (Exception e) { + throw handleException(e); + } + } + + private void cleanUpSensitiveData(Edge edge) { + edge.setEdgeLicenseKey(null); + edge.setRoutingKey(null); + edge.setSecret(null); + edge.setCloudEndpoint(null); + edge.setRootRuleChainId(null); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java new file mode 100644 index 0000000000..22191efddf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.edge.EdgeEvent; +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.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class EdgeEventController extends BaseController { + + @Autowired + private EdgeEventService edgeEventService; + + public static final String EDGE_ID = "edgeId"; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/events", method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeEvents( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 357036e4f4..3f072b77f0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -25,6 +25,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.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; @@ -63,10 +64,13 @@ public class EntityRelationController extends BaseController { relation.setTypeGroup(RelationTypeGroup.COMMON); } relationService.saveRelation(getTenantId(), relation); + logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); + + sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_ADD_OR_UPDATE); } catch (Exception e) { logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, e, relation); @@ -104,6 +108,8 @@ public class EntityRelationController extends BaseController { ActionType.RELATION_DELETED, null, relation); logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_DELETED, null, relation); + + sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_DELETED); } catch (Exception e) { logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_DELETED, e, relation); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 6250975b01..5e9103a66a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -32,11 +32,20 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -47,6 +56,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -65,6 +75,7 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; /** * Created by Victor Basanets on 8/28/2017. @@ -150,6 +161,11 @@ public class EntityViewController extends BaseController { logEntityAction(savedEntityView.getId(), savedEntityView, null, entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + if (entityView.getId() != null) { + sendEntityNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), EdgeEventActionType.UPDATED); + } + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), entityView, null, @@ -243,7 +259,7 @@ public class EntityViewController extends BaseController { private ListenableFuture> copyLatestFromEntityToEntityView(EntityView entityView, SecurityUser user) { EntityViewId entityId = entityView.getId(); List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? - entityView.getKeys().getTimeseries() : Collections.emptyList(); + entityView.getKeys().getTimeseries() : Collections.emptyList(); long startTs = entityView.getStartTimeMs(); long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); ListenableFuture> keysFuture; @@ -345,9 +361,14 @@ public class EntityViewController extends BaseController { try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); EntityView entityView = checkEntityViewId(entityViewId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), entityViewId); + entityViewService.deleteEntityView(getTenantId(), entityViewId); logEntityAction(entityViewId, entityView, entityView.getCustomerId(), ActionType.DELETED, null, strEntityViewId); + + sendDeleteNotificationMsg(getTenantId(), entityViewId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -388,6 +409,10 @@ public class EntityViewController extends BaseController { logEntityAction(entityViewId, savedEntityView, savedEntityView.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, strCustomerId, customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -414,6 +439,9 @@ public class EntityViewController extends BaseController { entityView.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEntityViewId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -585,4 +613,107 @@ public class EntityViewController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST) + @ResponseBody + public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + checkEntityViewId(entityViewId, Operation.ASSIGN_TO_EDGE); + + EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToEdge(getTenantId(), entityViewId, edgeId)); + logEntityAction(entityViewId, savedEntityView, + savedEntityView.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strEntityViewId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strEntityViewId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE) + @ResponseBody + public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + EntityView entityView = checkEntityViewId(entityViewId, Operation.UNASSIGN_FROM_EDGE); + + EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromEdge(getTenantId(), entityViewId, edgeId)); + logEntityAction(entityViewId, entityView, + entityView.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strEntityViewId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strEntityViewId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeEntityViews( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredEntityViews, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 3d31ba6c0c..6591a1577a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -19,11 +19,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; -import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -31,6 +35,7 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.utils.MiscUtils; import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; import java.util.List; @RestController @@ -46,6 +51,14 @@ public class OAuth2Controller extends BaseController { @ResponseBody public List getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { try { + if (log.isDebugEnabled()) { + log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = headerNames.nextElement(); + log.debug("Header: {} {}", header, request.getHeader(header)); + } + } return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request)); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index c36d328dd3..85ec0950e3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -42,7 +42,10 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,6 +57,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -138,13 +142,21 @@ public class RuleChainController extends BaseController { RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain)); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(), - created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + if (RuleChainType.CORE.equals(savedRuleChain.getType())) { + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + } logEntityAction(savedRuleChain.getId(), savedRuleChain, null, created ? ActionType.ADDED : ActionType.UPDATED, null); + if (RuleChainType.EDGE.equals(savedRuleChain.getType())) { + if (!created) { + sendEntityNotificationMsg(savedRuleChain.getTenantId(), savedRuleChain.getId(), EdgeEventActionType.UPDATED); + } + } + return savedRuleChain; } catch (Exception e) { @@ -234,12 +246,19 @@ public class RuleChainController extends BaseController { RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE); RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); + if (RuleChainType.CORE.equals(ruleChain.getType())) { + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); + } logEntityAction(ruleChain.getId(), ruleChain, null, ActionType.UPDATED, null, ruleChainMetaData); + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + sendEntityNotificationMsg(ruleChain.getTenantId(), + ruleChain.getId(), EdgeEventActionType.UPDATED); + } + return savedRuleChainMetaData; } catch (Exception e) { @@ -256,13 +275,18 @@ public class RuleChainController extends BaseController { public PageData getRuleChains( @RequestParam int pageSize, @RequestParam int page, + @RequestParam(value = "type", required = false) String typeStr, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(ruleChainService.findTenantRuleChains(tenantId, pageLink)); + RuleChainType type = RuleChainType.CORE; + if (typeStr != null && typeStr.trim().length() > 0) { + type = RuleChainType.valueOf(typeStr); + } + return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -281,19 +305,30 @@ public class RuleChainController extends BaseController { Set referencingRuleChainIds = referencingRuleNodes.stream().map(RuleNode::getRuleChainId).collect(Collectors.toSet()); + List relatedEdgeIds = null; + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + relatedEdgeIds = findRelatedEdgeIds(getTenantId(), ruleChainId); + } + ruleChainService.deleteRuleChainById(getTenantId(), ruleChainId); referencingRuleChainIds.remove(ruleChain.getId()); - referencingRuleChainIds.forEach(referencingRuleChainId -> - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED)); + if (RuleChainType.CORE.equals(ruleChain.getType())) { + referencingRuleChainIds.forEach(referencingRuleChainId -> + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED)); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED); + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED); + } logEntityAction(ruleChainId, ruleChain, null, ActionType.DELETED, null, strRuleChainId); + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + sendDeleteNotificationMsg(ruleChain.getTenantId(), ruleChain.getId(), relatedEdgeIds); + } + } catch (Exception e) { logEntityAction(emptyId(EntityType.RULE_CHAIN), null, @@ -411,7 +446,7 @@ public class RuleChainController extends BaseController { public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, RuleChainType.CORE, overwrite); if (!CollectionUtils.isEmpty(importResults)) { for (RuleChainImportResult importResult : importResults) { tbClusterService.onEntityStateChange(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent()); @@ -452,5 +487,160 @@ public class RuleChainController extends BaseController { return msgData; } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) + @ResponseBody + public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.ASSIGN_TO_EDGE); + + RuleChain savedRuleChain = checkNotNull(ruleChainService.assignRuleChainToEdge(getCurrentUser().getTenantId(), ruleChainId, edgeId)); + + logEntityAction(ruleChainId, savedRuleChain, + null, + ActionType.ASSIGNED_TO_EDGE, null, strRuleChainId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedRuleChain.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedRuleChain; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.RULE_CHAIN), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strRuleChainId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) + @ResponseBody + public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.UNASSIGN_FROM_EDGE); + + RuleChain savedRuleChain = checkNotNull(ruleChainService.unassignRuleChainFromEdge(getCurrentUser().getTenantId(), ruleChainId, edgeId, false)); + + logEntityAction(ruleChainId, ruleChain, + null, + ActionType.UNASSIGNED_FROM_EDGE, null, strRuleChainId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedRuleChain.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedRuleChain; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.RULE_CHAIN), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strRuleChainId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeRuleChains( + @PathVariable("edgeId") String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) + @ResponseBody + public RuleChain setEdgeTemplateRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.setEdgeTemplateRootRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) + @ResponseBody + public RuleChain setAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.setAutoAssignToEdgeRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) + @ResponseBody + public RuleChain unsetAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.unsetAutoAssignToEdgeRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) + @ResponseBody + public List getAutoAssignToEdgeRuleChains() throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId)).get(); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 6e2267b184..3ac679e0d7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -110,12 +110,11 @@ public class TbResourceController extends BaseController { @ResponseBody public TbResource saveResource(@RequestBody TbResource resource) throws ThingsboardException { try { - resource.setTenantId(getTenantId()); - checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); - TbResource savedResource = checkNotNull(resourceService.saveResource(resource)); - tbClusterService.onResourceChange(savedResource, null); - return savedResource; - } catch (Exception e) { + resource.setTenantId(getTenantId()); + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + return addResource(resource); + } + catch (Exception e) { throw handleException(e); } } @@ -183,4 +182,11 @@ public class TbResourceController extends BaseController { throw handleException(e); } } -} + + private TbResource addResource(TbResource resource) throws Exception { + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + TbResource savedResource = checkNotNull(resourceService.saveResource(resource)); + tbClusterService.onResourceChange(savedResource, null); + return savedResource; + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 70b5efe40d..7621d05908 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -95,6 +95,9 @@ public class TenantController extends BaseController { tenant = checkNotNull(tenantService.saveTenant(tenant)); if (newTenant) { installScripts.createDefaultRuleChains(tenant.getId()); + if (edgesEnabled) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } } tenantProfileCache.evict(tenant.getId()); tbClusterService.onTenantChange(tenant, null); diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index ca3ecdc36c..f254414902 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -36,9 +36,11 @@ import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +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.common.data.page.PageData; @@ -57,6 +59,7 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.List; @RequiredArgsConstructor @RestController @@ -178,6 +181,9 @@ public class UserController extends BaseController { savedUser.getCustomerId(), user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + sendEntityNotificationMsg(getTenantId(), savedUser.getId(), + user.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + return savedUser; } catch (Exception e) { @@ -247,12 +253,17 @@ public class UserController extends BaseController { try { UserId userId = new UserId(toUUID(strUserId)); User user = checkUserId(userId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), userId); + userService.deleteUser(getCurrentUser().getTenantId(), userId); logEntityAction(userId, user, user.getCustomerId(), ActionType.DELETED, null, strUserId); + sendDeleteNotificationMsg(getTenantId(), userId, relatedEdgeIds); + } catch (Exception e) { logEntityAction(emptyId(EntityType.USER), null, diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 3239d143bd..ad81821a82 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +26,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -39,6 +42,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +@Slf4j @RestController @TbCoreComponent @RequestMapping("/api") @@ -69,8 +73,12 @@ public class WidgetTypeController extends BaseController { } checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); + WidgetTypeDetails savedWidgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails); - return checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails)); + sendEntityNotificationMsg(getTenantId(), savedWidgetTypeDetails.getId(), + widgetTypeDetails.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + + return checkNotNull(savedWidgetTypeDetails); } catch (Exception e) { throw handleException(e); } @@ -85,6 +93,9 @@ public class WidgetTypeController extends BaseController { WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); checkWidgetTypeId(widgetTypeId, Operation.DELETE); widgetTypeService.deleteWidgetType(getCurrentUser().getTenantId(), widgetTypeId); + + sendEntityNotificationMsg(getTenantId(), widgetTypeId, EdgeEventActionType.DELETED); + } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 3908543922..14c5d6a860 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -68,7 +69,12 @@ public class WidgetsBundleController extends BaseController { } checkEntity(widgetsBundle.getId(), widgetsBundle, Resource.WIDGETS_BUNDLE); - return checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle)); + WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); + + sendEntityNotificationMsg(getTenantId(), savedWidgetsBundle.getId(), + widgetsBundle.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + + return checkNotNull(savedWidgetsBundle); } catch (Exception e) { throw handleException(e); } @@ -83,6 +89,9 @@ public class WidgetsBundleController extends BaseController { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); checkWidgetsBundleId(widgetsBundleId, Operation.DELETE); widgetsBundleService.deleteWidgetsBundle(getTenantId(), widgetsBundleId); + + sendEntityNotificationMsg(getTenantId(), widgetsBundleId, EdgeEventActionType.DELETED); + } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 57f1fd2927..ed8c0a7a17 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -27,6 +27,7 @@ import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; @@ -66,7 +67,12 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { response.setContentType(MediaType.APPLICATION_JSON_VALUE); if (exception instanceof ThingsboardException) { - handleThingsboardException((ThingsboardException) exception, response); + ThingsboardException thingsboardException = (ThingsboardException) exception; + if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { + handleSubscriptionException((ThingsboardException) exception, response); + } else { + handleThingsboardException((ThingsboardException) exception, response); + } } else if (exception instanceof TbRateLimitsException) { handleRateLimitException(response, (TbRateLimitsException) exception); } else if (exception instanceof AccessDeniedException) { @@ -126,6 +132,11 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS)); } + private void handleSubscriptionException(ThingsboardException subscriptionException, HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN.value()); + mapper.writeValue(response.getWriter(), + (new ObjectMapper()).readValue(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); + } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 4a9a110fab..3bc00ee5fa 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -193,7 +193,9 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.2.1"); case "3.2.2": log.info("Upgrading ThingsBoard from version 3.2.2 to 3.3.0 ..."); - databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); + databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); + + dataUpdateService.updateData("3.2.2"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index c0a9f5e7c5..00a5bca503 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -34,13 +34,13 @@ import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.component.ComponentDescriptorService; import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -65,7 +65,9 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe private Map components = new HashMap<>(); - private Map> componentsMap = new HashMap<>(); + private Map> coreComponentsMap = new HashMap<>(); + + private Map> edgeComponentsMap = new HashMap<>(); private ObjectMapper mapper = new ObjectMapper(); @@ -93,7 +95,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe ComponentType type = ruleNodeAnnotation.type(); ComponentDescriptor component = scanAndPersistComponent(def, type); components.put(component.getClazz(), component); - componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + putComponentIntoMaps(type, ruleNodeAnnotation, component); break; } catch (Exception e) { log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e); @@ -113,22 +115,35 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe } } - private void registerComponents(ComponentType type, Class annotation) { - List components = persist(getBeanDefinitions(annotation), type); - componentsMap.put(type, components); - registerComponents(components); - } - - private void registerComponents(Collection comps) { - comps.forEach(c -> components.put(c.getClazz(), c)); + private void putComponentIntoMaps(ComponentType type, RuleNode ruleNodeAnnotation, ComponentDescriptor component) { + boolean ruleChainTypesMethodAvailable; + try { + ruleNodeAnnotation.getClass().getMethod("ruleChainTypes"); + ruleChainTypesMethodAvailable = true; + } catch (NoSuchMethodException exception) { + log.warn("[{}] does not have ruleChainTypes. Probably extension class compiled before 3.3 release. " + + "Please update your extensions and compile using latest 3.3 release dependency", ruleNodeAnnotation.name()); + ruleChainTypesMethodAvailable = false; + } + if (ruleChainTypesMethodAvailable) { + if (ruleChainTypeContainsArray(RuleChainType.CORE, ruleNodeAnnotation.ruleChainTypes())) { + coreComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } + if (ruleChainTypeContainsArray(RuleChainType.EDGE, ruleNodeAnnotation.ruleChainTypes())) { + edgeComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } + } else { + coreComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } } - private List persist(Set filterDefs, ComponentType type) { - List result = new ArrayList<>(); - for (BeanDefinition def : filterDefs) { - result.add(scanAndPersistComponent(def, type)); + private boolean ruleChainTypeContainsArray(RuleChainType ruleChainType, RuleChainType[] array) { + for (RuleChainType tmp : array) { + if (ruleChainType.equals(tmp)) { + return true; + } } - return result; + return false; } private ComponentDescriptor scanAndPersistComponent(BeanDefinition def, ComponentType type) { @@ -222,25 +237,47 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe } @Override - public List getComponents(ComponentType type) { - if (componentsMap.containsKey(type)) { - return Collections.unmodifiableList(componentsMap.get(type)); + public List getComponents(ComponentType type, RuleChainType ruleChainType) { + if (RuleChainType.CORE.equals(ruleChainType)) { + if (coreComponentsMap.containsKey(type)) { + return Collections.unmodifiableList(coreComponentsMap.get(type)); + } else { + return Collections.emptyList(); + } + } else if (RuleChainType.EDGE.equals(ruleChainType)) { + if (edgeComponentsMap.containsKey(type)) { + return Collections.unmodifiableList(edgeComponentsMap.get(type)); + } else { + return Collections.emptyList(); + } } else { - return Collections.emptyList(); + log.error("Unsupported rule chain type {}", ruleChainType); + throw new RuntimeException("Unsupported rule chain type " + ruleChainType); } } @Override - public List getComponents(Set types) { - List result = new ArrayList<>(); - types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> { - result.addAll(componentsMap.get(type)); - }); - return Collections.unmodifiableList(result); + public List getComponents(Set types, RuleChainType ruleChainType) { + if (RuleChainType.CORE.equals(ruleChainType)) { + return getComponents(types, coreComponentsMap); + } else if (RuleChainType.EDGE.equals(ruleChainType)) { + return getComponents(types, edgeComponentsMap); + } else { + log.error("Unsupported rule chain type {}", ruleChainType); + throw new RuntimeException("Unsupported rule chain type " + ruleChainType); + } } @Override public Optional getComponent(String clazz) { return Optional.ofNullable(components.get(clazz)); } + + private List getComponents(Set types, Map> componentsMap) { + List result = new ArrayList<>(); + types.stream().filter(componentsMap::containsKey).forEach(type -> { + result.addAll(componentsMap.get(type)); + }); + return Collections.unmodifiableList(result); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java index 4d62096cdd..d5b4ed5e32 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.component; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; import java.util.Optional; @@ -29,10 +30,9 @@ public interface ComponentDiscoveryService { void discoverComponents(); - List getComponents(ComponentType type); + List getComponents(ComponentType type, RuleChainType ruleChainType); - List getComponents(Set types); + List getComponents(Set types, RuleChainType ruleChainType); Optional getComponent(String clazz); - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java new file mode 100644 index 0000000000..66e64c14f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -0,0 +1,505 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EdgeUtils; +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.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Service +@TbCoreComponent +@Slf4j +public class DefaultEdgeNotificationService implements EdgeNotificationService { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + @Autowired + private EdgeService edgeService; + + @Autowired + private AlarmService alarmService; + + @Autowired + private UserService userService; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private TbClusterService clusterService; + + @Autowired + private DbCallbackExecutorService dbCallbackExecutorService; + + private ExecutorService tsCallBackExecutor; + + @PostConstruct + public void initExecutor() { + tsCallBackExecutor = Executors.newSingleThreadExecutor(); + } + + @PreDestroy + public void shutdownExecutor() { + if (tsCallBackExecutor != null) { + tsCallBackExecutor.shutdownNow(); + } + } + + @Override + public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws IOException { + edge.setRootRuleChainId(ruleChainId); + Edge savedEdge = edgeService.saveEdge(edge); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.UPDATED, ruleChainId, null); + return savedEdge; + } + + private void saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.debug("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + clusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + + } + + @Override + public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { + try { + TenantId tenantId = new TenantId(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + switch (type) { + case EDGE: + processEdge(tenantId, edgeNotificationMsg); + break; + case USER: + case ASSET: + case DEVICE: + case DEVICE_PROFILE: + case ENTITY_VIEW: + case DASHBOARD: + case RULE_CHAIN: + processEntity(tenantId, edgeNotificationMsg); + break; + case CUSTOMER: + processCustomer(tenantId, edgeNotificationMsg); + break; + case WIDGETS_BUNDLE: + case WIDGET_TYPE: + processWidgetBundleOrWidgetType(tenantId, edgeNotificationMsg); + break; + case ALARM: + processAlarm(tenantId, edgeNotificationMsg); + break; + case RELATION: + processRelation(tenantId, edgeNotificationMsg); + break; + default: + log.debug("Edge event type [{}] is not designed to be pushed to edge", type); + } + } catch (Exception e) { + callback.onFailure(e); + log.error("Can't push to edge updates, edgeNotificationMsg [{}]", edgeNotificationMsg, e); + } finally { + callback.onSuccess(); + } + } + + private void processEdge(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + try { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + ListenableFuture edgeFuture; + switch (actionType) { + case ASSIGNED_TO_CUSTOMER: + CustomerId customerId = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + edgeFuture = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(edgeFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && !customerId.isNullUid()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be added to edge.", edge.getId(), pageData.getData().size()); + for (User user : pageData.getData()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't find edge by id [{}]", edgeNotificationMsg, t); + } + }, dbCallbackExecutorService); + break; + case UNASSIGNED_FROM_CUSTOMER: + CustomerId customerIdToDelete = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + edgeFuture = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(edgeFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && !customerIdToDelete.isNullUid()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.DELETED, customerIdToDelete, null); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't find edge by id [{}]", edgeNotificationMsg, t); + } + }, dbCallbackExecutorService); + break; + } + } catch (Exception e) { + log.error("Exception during processing edge event", e); + } + } + + private void processWidgetBundleOrWidgetType(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + switch (actionType) { + case ADDED: + case UPDATED: + case DELETED: + processActionForAllEdges(tenantId, type, actionType, entityId); + break; + } + } + + private void processCustomer(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); + CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); + switch (actionType) { + case UPDATED: + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + break; + case DELETED: + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); + break; + } + } + + private void processEntity(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, + new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + ListenableFuture> edgeIdsFuture; + switch (actionType) { + case ADDED: // used only for USER entity + case UPDATED: + case CREDENTIALS_UPDATED: + edgeIdsFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId); + Futures.addCallback(edgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + } + } + } + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find related edge ids [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + break; + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + edgeIdsFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId); + Futures.addCallback(edgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + try { + CustomerId customerId = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + ListenableFuture future = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && edge.getCustomerId() != null && + !edge.getCustomerId().isNullUid() && edge.getCustomerId().equals(customerId)) { + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + } + } + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find edge by id [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + } catch (Exception e) { + log.error("Can't parse customer id from entity body [{}]", edgeNotificationMsg, e); + } + } + } + } + + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find related edge ids [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + break; + case DELETED: + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + break; + case ASSIGNED_TO_EDGE: + case UNASSIGNED_FROM_EDGE: + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + if (type.equals(EdgeEventType.RULE_CHAIN)) { + updateDependentRuleChains(tenantId, new RuleChainId(entityId.getId()), edgeId); + } + break; + } + } + + private void updateDependentRuleChains(TenantId tenantId, RuleChainId processingRuleChainId, EdgeId edgeId) { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (RuleChain ruleChain : pageData.getData()) { + if (!ruleChain.getId().equals(processingRuleChainId)) { + List connectionInfos = + ruleChainService.loadRuleChainMetaData(ruleChain.getTenantId(), ruleChain.getId()).getRuleChainConnections(); + if (connectionInfos != null && !connectionInfos.isEmpty()) { + for (RuleChainConnectionInfo connectionInfo : connectionInfos) { + if (connectionInfo.getTargetRuleChainId().equals(processingRuleChainId)) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RULE_CHAIN_METADATA, + EdgeEventActionType.UPDATED, + ruleChain.getId(), + null); + } + } + } + } + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } + + private void processAlarm(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); + Futures.addCallback(alarmFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Alarm alarm) { + if (alarm != null) { + EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType()); + if (type != null) { + ListenableFuture> relatedEdgeIdsByEntityIdFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, alarm.getOriginator()); + Futures.addCallback(relatedEdgeIdsByEntityIdFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List relatedEdgeIdsByEntityId) { + if (relatedEdgeIdsByEntityId != null) { + for (EdgeId edgeId : relatedEdgeIdsByEntityId) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.ALARM, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + alarmId, + null); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find related edge ids by entity id [{}]", tenantId.getId(), alarm.getOriginator(), t); + } + }, dbCallbackExecutorService); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find alarm by id [{}]", tenantId.getId(), alarmId.getId(), t); + } + }, dbCallbackExecutorService); + } + + private void processRelation(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { + EntityRelation relation = mapper.readValue(edgeNotificationMsg.getBody(), EntityRelation.class); + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + List>> futures = new ArrayList<>(); + futures.add(edgeService.findRelatedEdgeIdsByEntityId(tenantId, relation.getTo())); + futures.add(edgeService.findRelatedEdgeIdsByEntityId(tenantId, relation.getFrom())); + ListenableFuture>> combinedFuture = Futures.allAsList(futures); + Futures.addCallback(combinedFuture, new FutureCallback>>() { + @Override + public void onSuccess(@Nullable List> listOfListsEdgeIds) { + Set uniqueEdgeIds = new HashSet<>(); + if (listOfListsEdgeIds != null && !listOfListsEdgeIds.isEmpty()) { + for (List listOfListsEdgeId : listOfListsEdgeIds) { + if (listOfListsEdgeId != null) { + uniqueEdgeIds.addAll(listOfListsEdgeId); + } + } + } + if (!uniqueEdgeIds.isEmpty()) { + for (EdgeId edgeId : uniqueEdgeIds) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RELATION, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + null, + mapper.valueToTree(relation)); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find related edge ids by relation to id [{}] and relation from id [{}]" , + tenantId.getId(), relation.getTo().getId(), relation.getFrom().getId(), t); + } + }, dbCallbackExecutorService); + } + } + + private void processActionForAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId) { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } +} + + diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java new file mode 100644 index 0000000000..eb3852f7ac --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -0,0 +1,233 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import lombok.Data; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.actors.service.ActorService; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; +import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; +import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService; +import org.thingsboard.server.service.edge.rpc.processor.AlarmProcessor; +import org.thingsboard.server.service.edge.rpc.processor.DeviceProcessor; +import org.thingsboard.server.service.edge.rpc.processor.RelationProcessor; +import org.thingsboard.server.service.edge.rpc.processor.TelemetryProcessor; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.state.DeviceStateService; + +@Component +@TbCoreComponent +@Data +public class EdgeContextComponent { + + @Lazy + @Autowired + private EdgeService edgeService; + + @Autowired + private PartitionService partitionService; + + @Lazy + @Autowired + private EdgeEventService edgeEventService; + + @Lazy + @Autowired + private AssetService assetService; + + @Lazy + @Autowired + private DeviceService deviceService; + + @Lazy + @Autowired + private DeviceProfileService deviceProfileService; + + @Lazy + @Autowired + private DeviceCredentialsService deviceCredentialsService; + + @Lazy + @Autowired + private EntityViewService entityViewService; + + @Lazy + @Autowired + private AttributesService attributesService; + + @Lazy + @Autowired + private CustomerService customerService; + + @Lazy + @Autowired + private RelationService relationService; + + @Lazy + @Autowired + private AlarmService alarmService; + + @Lazy + @Autowired + private DashboardService dashboardService; + + @Lazy + @Autowired + private RuleChainService ruleChainService; + + @Lazy + @Autowired + private UserService userService; + + @Lazy + @Autowired + private ActorService actorService; + + @Lazy + @Autowired + private WidgetsBundleService widgetsBundleService; + + @Lazy + @Autowired + private WidgetTypeService widgetTypeService; + + @Lazy + @Autowired + private DeviceStateService deviceStateService; + + @Lazy + @Autowired + private TbClusterService tbClusterService; + + @Lazy + @Autowired + private SyncEdgeService syncEdgeService; + + @Lazy + @Autowired + private RuleChainMsgConstructor ruleChainMsgConstructor; + + @Lazy + @Autowired + private AlarmMsgConstructor alarmMsgConstructor; + + @Lazy + @Autowired + private DeviceMsgConstructor deviceMsgConstructor; + + @Lazy + @Autowired + private DeviceProfileMsgConstructor deviceProfileMsgConstructor; + + @Lazy + @Autowired + private AssetMsgConstructor assetMsgConstructor; + + @Lazy + @Autowired + private EntityViewMsgConstructor entityViewMsgConstructor; + + @Lazy + @Autowired + private DashboardMsgConstructor dashboardMsgConstructor; + + @Lazy + @Autowired + private CustomerMsgConstructor customerMsgConstructor; + + @Lazy + @Autowired + private UserMsgConstructor userMsgConstructor; + + @Lazy + @Autowired + private RelationMsgConstructor relationMsgConstructor; + + @Lazy + @Autowired + private WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; + + @Lazy + @Autowired + private WidgetTypeMsgConstructor widgetTypeMsgConstructor; + + @Lazy + @Autowired + private AdminSettingsMsgConstructor adminSettingsMsgConstructor; + + @Lazy + @Autowired + private EntityDataMsgConstructor entityDataMsgConstructor; + + @Lazy + @Autowired + private AlarmProcessor alarmProcessor; + + @Lazy + @Autowired + private DeviceProcessor deviceProcessor; + + @Lazy + @Autowired + private RelationProcessor relationProcessor; + + @Lazy + @Autowired + private TelemetryProcessor telemetryProcessor; + + @Lazy + @Autowired + private EdgeEventStorageSettings edgeEventStorageSettings; + + @Autowired + @Getter + private DbCallbackExecutorService dbCallbackExecutor; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java new file mode 100644 index 0000000000..48463d246c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.io.IOException; + +public interface EdgeNotificationService { + + Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws IOException; + + void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java new file mode 100644 index 0000000000..621f7e60e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc; + + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Data +public class EdgeEventStorageSettings { + @Value("${edges.storage.max_read_records_count}") + private int maxReadRecordsCount; + @Value("${edges.storage.no_read_records_sleep}") + private long noRecordsSleepInterval; + @Value("${edges.storage.sleep_between_batches}") + private long sleepIntervalBetweenBatches; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java new file mode 100644 index 0000000000..70033f10e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -0,0 +1,290 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.FutureCallback; +import io.grpc.Server; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.gen.edge.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.state.DefaultDeviceStateService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentMap sessionNewEvents = new ConcurrentHashMap<>(); + private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); + private static final ObjectMapper mapper = new ObjectMapper(); + + @Value("${edges.rpc.port}") + private int rpcPort; + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${edges.rpc.ssl.cert}") + private String certFileResource; + @Value("${edges.rpc.ssl.private_key}") + private String privateKeyResource; + @Value("${edges.state.persistToTelemetry:false}") + private boolean persistToTelemetry; + @Value("${edges.rpc.client_max_keep_alive_time_sec}") + private int clientMaxKeepAliveTimeSec; + + @Value("${edges.scheduler_pool_size}") + private int schedulerPoolSize; + + @Autowired + private EdgeContextComponent ctx; + + @Autowired + private TelemetrySubscriptionService tsSubService; + + private Server server; + + private ScheduledExecutorService scheduler; + + @PostConstruct + public void init() { + log.info("Initializing Edge RPC service!"); + NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) + .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .addService(this); + if (sslEnabled) { + try { + File certFile = new File(Resources.getResource(certFileResource).toURI()); + File privateKeyFile = new File(Resources.getResource(privateKeyResource).toURI()); + builder.useTransportSecurity(certFile, privateKeyFile); + } catch (Exception e) { + log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); + throw new RuntimeException("Unable to set up SSL context!", e); + } + } + server = builder.build(); + log.info("Going to start Edge RPC server using port: {}", rpcPort); + try { + server.start(); + } catch (IOException e) { + log.error("Failed to start Edge RPC server!", e); + throw new RuntimeException("Failed to start Edge RPC server!"); + } + this.scheduler = Executors.newScheduledThreadPool(schedulerPoolSize, ThingsBoardThreadFactory.forName("edge-scheduler")); + log.info("Edge RPC service initialized!"); + } + + @PreDestroy + public void destroy() { + if (server != null) { + server.shutdownNow(); + } + for (Map.Entry> entry : sessionEdgeEventChecks.entrySet()) { + EdgeId edgeId = entry.getKey(); + ScheduledFuture sessionEdgeEventCheck = entry.getValue(); + if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { + sessionEdgeEventCheck.cancel(true); + sessionEdgeEventChecks.remove(edgeId); + } + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + + @Override + public StreamObserver handleMsgs(StreamObserver outputStream) { + return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, mapper).getInputStream(); + } + + @Override + public void updateEdge(Edge edge) { + EdgeGrpcSession session = sessions.get(edge.getId()); + if (session != null && session.isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", edge.getTenantId(), edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", edge.getTenantId(), edge.getName(), edge.getId()); + } + } + + @Override + public void deleteEdge(EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.info("Closing and removing session for edge [{}]", edgeId); + session.close(); + sessions.remove(edgeId); + sessionNewEvents.remove(edgeId); + cancelScheduleEdgeEventsCheck(edgeId); + } + } + + @Override + public void onEdgeEvent(EdgeId edgeId) { + log.trace("[{}] onEdgeEvent", edgeId.getId()); + if (!sessionNewEvents.get(edgeId)) { + log.trace("[{}] set session new events flag to true", edgeId.getId()); + sessionNewEvents.put(edgeId, true); + } + } + + private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { + log.info("[{}] edge [{}] connected successfully.", edgeGrpcSession.getSessionId(), edgeId); + sessions.put(edgeId, edgeGrpcSession); + sessionNewEvents.put(edgeId, false); + save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); + save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, System.currentTimeMillis()); + cancelScheduleEdgeEventsCheck(edgeId); + scheduleEdgeEventsCheck(edgeGrpcSession); + } + + public EdgeGrpcSession getEdgeGrpcSessionById(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + return session; + } else { + log.error("[{}] Edge is not connected [{}]", tenantId, edgeId); + throw new RuntimeException("Edge is not connected"); + } + } + + private void scheduleEdgeEventsCheck(EdgeGrpcSession session) { + EdgeId edgeId = session.getEdge().getId(); + UUID tenantId = session.getEdge().getTenantId().getId(); + if (sessions.containsKey(edgeId)) { + ScheduledFuture schedule = scheduler.schedule(() -> { + try { + if (sessionNewEvents.get(edgeId)) { + log.trace("[{}] Set session new events flag to false", edgeId.getId()); + sessionNewEvents.put(edgeId, false); + session.processEdgeEvents(); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), e); + } + scheduleEdgeEventsCheck(session); + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + sessionEdgeEventChecks.put(edgeId, schedule); + log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); + } else { + log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", + tenantId, edgeId.getId()); + } + } + + private void cancelScheduleEdgeEventsCheck(EdgeId edgeId) { + log.trace("[{}] cancelling edge event check for edge", edgeId); + if (sessionEdgeEventChecks.containsKey(edgeId)) { + ScheduledFuture sessionEdgeEventCheck = sessionEdgeEventChecks.get(edgeId); + if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { + sessionEdgeEventCheck.cancel(true); + sessionEdgeEventChecks.remove(edgeId); + } + } + } + + private void onEdgeDisconnect(EdgeId edgeId) { + log.info("[{}] edge disconnected!", edgeId); + sessions.remove(edgeId); + sessionNewEvents.remove(edgeId); + save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); + save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, System.currentTimeMillis()); + cancelScheduleEdgeEventsCheck(edgeId); + } + + private void save(EdgeId edgeId, String key, long value) { + log.debug("[{}] Updating long edge telemetry [{}] [{}]", edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, edgeId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))), + new AttributeSaveCallback(edgeId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + } + } + + private void save(EdgeId edgeId, String key, boolean value) { + log.debug("[{}] Updating boolean edge telemetry [{}] [{}]", edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, edgeId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), + new AttributeSaveCallback(edgeId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + } + } + + private static class AttributeSaveCallback implements FutureCallback { + private final EdgeId edgeId; + private final String key; + private final Object value; + + AttributeSaveCallback(EdgeId edgeId, String key, Object value) { + this.edgeId = edgeId; + this.key = key; + this.value = value; + } + + @Override + public void onSuccess(@Nullable Void result) { + log.trace("[{}] Successfully updated attribute [{}] with value [{}]", edgeId, key, value); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java new file mode 100644 index 0000000000..70978df0a3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -0,0 +1,1045 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonElement; +import io.grpc.stub.StreamObserver; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +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.EntityView; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.ConnectResponseCode; +import org.thingsboard.server.gen.edge.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EdgeUpdateMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.RequestMsgType; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@Slf4j +@Data +public final class EdgeGrpcSession implements Closeable { + + private static final ReentrantLock downlinkMsgLock = new ReentrantLock(); + + private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; + + private final UUID sessionId; + private final BiConsumer sessionOpenListener; + private final Consumer sessionCloseListener; + private final ObjectMapper mapper; + + private EdgeContextComponent ctx; + private Edge edge; + private StreamObserver inputStream; + private StreamObserver outputStream; + private boolean connected; + + private CountDownLatch latch; + + EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, + Consumer sessionCloseListener, ObjectMapper mapper) { + this.sessionId = UUID.randomUUID(); + this.ctx = ctx; + this.outputStream = outputStream; + this.sessionOpenListener = sessionOpenListener; + this.sessionCloseListener = sessionCloseListener; + this.mapper = mapper; + initInputStream(); + } + + private void initInputStream() { + this.inputStream = new StreamObserver() { + @Override + public void onNext(RequestMsg requestMsg) { + if (!connected && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { + ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); + outputStream.onNext(ResponseMsg.newBuilder() + .setConnectResponseMsg(responseMsg) + .build()); + if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { + outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); + } else { + connected = true; + } + } + if (connected && requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { + ctx.getSyncEdgeService().sync(edge.getTenantId(), edge); + } + if (connected) { + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE) && requestMsg.hasUplinkMsg()) { + onUplinkMsg(requestMsg.getUplinkMsg()); + } + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE) && requestMsg.hasDownlinkResponseMsg()) { + onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); + } + } + } + + @Override + public void onError(Throwable t) { + log.error("Failed to deliver message from client!", t); + closeSession(); + } + + @Override + public void onCompleted() { + closeSession(); + } + + private void closeSession() { + connected = false; + if (edge != null) { + try { + sessionCloseListener.accept(edge.getId()); + } catch (Exception ignored) {} + } + try { + outputStream.onCompleted(); + } catch (Exception ignored) {} + } + }; + } + + private void onUplinkMsg(UplinkMsg uplinkMsg) { + ListenableFuture> future = processUplinkMsg(uplinkMsg); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder().setSuccess(true).build(); + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(uplinkResponseMsg) + .build()); + } + + @Override + public void onFailure(Throwable t) { + UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder().setSuccess(false).setErrorMsg(t.getMessage()).build(); + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(uplinkResponseMsg) + .build()); + } + }, MoreExecutors.directExecutor()); + } + + private void onDownlinkResponse(DownlinkResponseMsg msg) { + try { + if (msg.getSuccess()) { + log.debug("[{}] Msg has been processed successfully! {}", edge.getRoutingKey(), msg); + } else { + log.error("[{}] Msg processing failed! Error msg: {}", edge.getRoutingKey(), msg.getErrorMsg()); + } + latch.countDown(); + } catch (Exception e) { + log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); + } + } + + private void sendDownlinkMsg(ResponseMsg downlinkMsg) { + log.trace("[{}] Sending downlink msg [{}]", this.sessionId, downlinkMsg); + if (isConnected()) { + try { + downlinkMsgLock.lock(); + outputStream.onNext(downlinkMsg); + } catch (Exception e) { + log.error("[{}] Failed to send downlink message [{}]", this.sessionId, downlinkMsg, e); + connected = false; + sessionCloseListener.accept(edge.getId()); + } finally { + downlinkMsgLock.unlock(); + } + log.trace("[{}] Response msg successfully sent [{}]", this.sessionId, downlinkMsg); + } + } + + void onConfigurationUpdate(Edge edge) { + log.debug("[{}] onConfigurationUpdate [{}]", this.sessionId, edge); + this.edge = edge; + EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() + .setConfiguration(constructEdgeConfigProto(edge)).build(); + ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() + .setEdgeUpdateMsg(edgeConfig) + .build(); + sendDownlinkMsg(edgeConfigMsg); + } + + void processEdgeEvents() throws ExecutionException, InterruptedException { + log.trace("[{}] processHandleMessages started", this.sessionId); + if (isConnected()) { + Long queueStartTs = getQueueStartTs().get(); + TimePageLink pageLink = new TimePageLink( + ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), + 0, + null, + new SortOrder("createdTime", SortOrder.Direction.ASC), + queueStartTs, + null); + PageData pageData; + UUID ifOffset = null; + boolean success = true; + do { + pageData = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), pageLink, true); + if (isConnected() && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); + List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); + log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, downlinkMsgsPack.size()); + + latch = new CountDownLatch(downlinkMsgsPack.size()); + for (DownlinkMsg downlinkMsg : downlinkMsgsPack) { + sendDownlinkMsg(ResponseMsg.newBuilder() + .setDownlinkMsg(downlinkMsg) + .build()); + } + + ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); + + success = latch.await(10, TimeUnit.SECONDS); + if (!success) { + log.warn("[{}] Failed to deliver the batch: {}", this.sessionId, downlinkMsgsPack); + } + } + if (isConnected() && (!success || pageData.hasNext())) { + try { + Thread.sleep(ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches()); + } catch (InterruptedException e) { + log.error("[{}] Error during sleep between batches", this.sessionId, e); + } + if (success) { + pageLink = pageLink.nextPageLink(); + } + } + } while (isConnected() && (!success || pageData.hasNext())); + + if (ifOffset != null) { + Long newStartTs = Uuids.unixTimestamp(ifOffset); + updateQueueStartTs(newStartTs); + } + } + log.trace("[{}] processHandleMessages finished", this.sessionId); + } + + private List convertToDownlinkMsgsPack(List edgeEvents) { + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + log.trace("[{}] Processing edge event [{}]", this.sessionId, edgeEvent); + try { + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case UPDATED: + case ADDED: + case DELETED: + case ASSIGNED_TO_EDGE: + case UNASSIGNED_FROM_EDGE: + case ALARM_ACK: + case ALARM_CLEAR: + case CREDENTIALS_UPDATED: + case RELATION_ADD_OR_UPDATE: + case RELATION_DELETED: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + downlinkMsg = processEntityMessage(edgeEvent, edgeEvent.getAction()); + break; + case ATTRIBUTES_UPDATED: + case POST_ATTRIBUTES: + case ATTRIBUTES_DELETED: + case TIMESERIES_UPDATED: + downlinkMsg = processTelemetryMessage(edgeEvent); + break; + case CREDENTIALS_REQUEST: + downlinkMsg = processCredentialsRequestMessage(edgeEvent); + break; + case ENTITY_MERGE_REQUEST: + downlinkMsg = processEntityMergeRequestMessage(edgeEvent); + break; + case RPC_CALL: + downlinkMsg = processRpcCallMsg(edgeEvent); + break; + } + if (downlinkMsg != null) { + result.add(downlinkMsg); + } + } catch (Exception e) { + log.error("Exception during processing records from queue", e); + } + } + return result; + } + + private DownlinkMsg processEntityMergeRequestMessage(EdgeEvent edgeEvent) { + DownlinkMsg downlinkMsg = null; + if (EdgeEventType.DEVICE.equals(edgeEvent.getType())) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + Device device = ctx.getDeviceService().findDeviceById(edge.getTenantId(), deviceId); + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(device); + String conflictName = null; + if(edgeEvent.getBody() != null) { + conflictName = edgeEvent.getBody().get("conflictName").asText(); + } + DeviceUpdateMsg d = ctx.getDeviceMsgConstructor() + .constructDeviceUpdatedMsg(UpdateMsgType.ENTITY_MERGE_RPC_MESSAGE, device, customerId, conflictName); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(d)) + .build(); + } + return downlinkMsg; + } + + private DownlinkMsg processRpcCallMsg(EdgeEvent edgeEvent) { + log.trace("Executing processRpcCall, edgeEvent [{}]", edgeEvent); + DeviceRpcCallMsg deviceRpcCallMsg = + ctx.getDeviceMsgConstructor().constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); + return DownlinkMsg.newBuilder() + .addAllDeviceRpcCallMsg(Collections.singletonList(deviceRpcCallMsg)) + .build(); + } + + private DownlinkMsg processCredentialsRequestMessage(EdgeEvent edgeEvent) { + DownlinkMsg downlinkMsg = null; + if (EdgeEventType.DEVICE.equals(edgeEvent.getType())) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .build(); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .addAllDeviceCredentialsRequestMsg(Collections.singletonList(deviceCredentialsRequestMsg)); + downlinkMsg = builder.build(); + } + return downlinkMsg; + } + + private ListenableFuture getQueueStartTs() { + ListenableFuture> future = + ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY); + return Futures.transform(future, attributeKvEntryOpt -> { + if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) { + AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get(); + return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } else { + return 0L; + } + }, ctx.getDbCallbackExecutor()); + } + + private void updateQueueStartTs(Long newStartTs) { + log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); + newStartTs = ++newStartTs; // increments ts by 1 - next edge event search starts from current offset + 1 + List attributes = Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis())); + ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes); + } + + private DownlinkMsg processTelemetryMessage(EdgeEvent edgeEvent) { + log.trace("[{}] Executing processTelemetryMessage, edgeEvent [{}]", this.sessionId, edgeEvent); + EntityId entityId = null; + switch (edgeEvent.getType()) { + case DEVICE: + entityId = new DeviceId(edgeEvent.getEntityId()); + break; + case ASSET: + entityId = new AssetId(edgeEvent.getEntityId()); + break; + case ENTITY_VIEW: + entityId = new EntityViewId(edgeEvent.getEntityId()); + break; + case DASHBOARD: + entityId = new DashboardId(edgeEvent.getEntityId()); + break; + case TENANT: + entityId = new TenantId(edgeEvent.getEntityId()); + break; + case CUSTOMER: + entityId = new CustomerId(edgeEvent.getEntityId()); + break; + case EDGE: + entityId = new EdgeId(edgeEvent.getEntityId()); + break; + } + DownlinkMsg downlinkMsg = null; + if (entityId != null) { + log.debug("[{}] Sending telemetry data msg, entityId [{}], body [{}]", this.sessionId, edgeEvent.getEntityId(), edgeEvent.getBody()); + try { + downlinkMsg = constructEntityDataProtoMsg(entityId, edgeEvent.getAction(), JsonUtils.parse(mapper.writeValueAsString(edgeEvent.getBody()))); + } catch (Exception e) { + log.warn("[{}] Can't send telemetry data msg, entityId [{}], body [{}]", this.sessionId, edgeEvent.getEntityId(), edgeEvent.getBody(), e); + } + } + return downlinkMsg; + } + + private DownlinkMsg processEntityMessage(EdgeEvent edgeEvent, EdgeEventActionType action) { + UpdateMsgType msgType = getResponseMsgType(edgeEvent.getAction()); + log.trace("Executing processEntityMessage, edgeEvent [{}], action [{}], msgType [{}]", edgeEvent, action, msgType); + switch (edgeEvent.getType()) { + case DEVICE: + return processDevice(edgeEvent, msgType, action); + case DEVICE_PROFILE: + return processDeviceProfile(edgeEvent, msgType, action); + case ASSET: + return processAsset(edgeEvent, msgType, action); + case ENTITY_VIEW: + return processEntityView(edgeEvent, msgType, action); + case DASHBOARD: + return processDashboard(edgeEvent, msgType, action); + case CUSTOMER: + return processCustomer(edgeEvent, msgType, action); + case RULE_CHAIN: + return processRuleChain(edgeEvent, msgType, action); + case RULE_CHAIN_METADATA: + return processRuleChainMetadata(edgeEvent, msgType); + case ALARM: + return processAlarm(edgeEvent, msgType); + case USER: + return processUser(edgeEvent, msgType, action); + case RELATION: + return processRelation(edgeEvent, msgType); + case WIDGETS_BUNDLE: + return processWidgetsBundle(edgeEvent, msgType, action); + case WIDGET_TYPE: + return processWidgetType(edgeEvent, msgType, action); + case ADMIN_SETTINGS: + return processAdminSettings(edgeEvent); + default: + log.warn("Unsupported edge event type [{}]", edgeEvent); + return null; + } + } + + private DownlinkMsg processDevice(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Device device = ctx.getDeviceService().findDeviceById(edgeEvent.getTenantId(), deviceId); + if (device != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(device); + DeviceUpdateMsg deviceUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceUpdatedMsg(msgType, device, customerId, null); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(deviceUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DeviceUpdateMsg deviceUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceDeleteMsg(deviceId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(deviceUpdateMsg)) + .build(); + break; + case CREDENTIALS_UPDATED: + DeviceCredentials deviceCredentials = ctx.getDeviceCredentialsService().findDeviceCredentialsByDeviceId(edge.getTenantId(), deviceId); + if (deviceCredentials != null) { + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceCredentialsUpdatedMsg(deviceCredentials); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceCredentialsUpdateMsg(Collections.singletonList(deviceCredentialsUpdateMsg)) + .build(); + } + break; + } + log.trace("[{}] device processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processDeviceProfile(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + DeviceProfile deviceProfile = ctx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); + if (deviceProfile != null) { + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + ctx.getDeviceProfileMsgConstructor().constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceProfileUpdateMsg(Collections.singletonList(deviceProfileUpdateMsg)) + .build(); + } + break; + case DELETED: + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + ctx.getDeviceProfileMsgConstructor().constructDeviceProfileDeleteMsg(deviceProfileId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceProfileUpdateMsg(Collections.singletonList(deviceProfileUpdateMsg)) + .build(); + break; + } + log.trace("[{}] device profile processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAsset(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + AssetId assetId = new AssetId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Asset asset = ctx.getAssetService().findAssetById(edgeEvent.getTenantId(), assetId); + if (asset != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(asset); + AssetUpdateMsg assetUpdateMsg = + ctx.getAssetMsgConstructor().constructAssetUpdatedMsg(msgType, asset, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAssetUpdateMsg(Collections.singletonList(assetUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + AssetUpdateMsg assetUpdateMsg = + ctx.getAssetMsgConstructor().constructAssetDeleteMsg(assetId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAssetUpdateMsg(Collections.singletonList(assetUpdateMsg)) + .build(); + break; + } + log.trace("[{}] asset processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processEntityView(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + EntityView entityView = ctx.getEntityViewService().findEntityViewById(edgeEvent.getTenantId(), entityViewId); + if (entityView != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(entityView); + EntityViewUpdateMsg entityViewUpdateMsg = + ctx.getEntityViewMsgConstructor().constructEntityViewUpdatedMsg(msgType, entityView, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityViewUpdateMsg(Collections.singletonList(entityViewUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + EntityViewUpdateMsg entityViewUpdateMsg = + ctx.getEntityViewMsgConstructor().constructEntityViewDeleteMsg(entityViewId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityViewUpdateMsg(Collections.singletonList(entityViewUpdateMsg)) + .build(); + break; + } + log.trace("[{}] entity view processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processDashboard(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Dashboard dashboard = ctx.getDashboardService().findDashboardById(edgeEvent.getTenantId(), dashboardId); + if (dashboard != null) { + CustomerId customerId = null; + if (!edge.getCustomerId().isNullUid() && dashboard.isAssignedToCustomer(edge.getCustomerId())) { + customerId = edge.getCustomerId(); + } + DashboardUpdateMsg dashboardUpdateMsg = + ctx.getDashboardMsgConstructor().constructDashboardUpdatedMsg(msgType, dashboard, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDashboardUpdateMsg(Collections.singletonList(dashboardUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DashboardUpdateMsg dashboardUpdateMsg = + ctx.getDashboardMsgConstructor().constructDashboardDeleteMsg(dashboardId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDashboardUpdateMsg(Collections.singletonList(dashboardUpdateMsg)) + .build(); + break; + } + log.trace("[{}] dashboard processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processCustomer(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + Customer customer = ctx.getCustomerService().findCustomerById(edgeEvent.getTenantId(), customerId); + if (customer != null) { + CustomerUpdateMsg customerUpdateMsg = + ctx.getCustomerMsgConstructor().constructCustomerUpdatedMsg(msgType, customer); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllCustomerUpdateMsg(Collections.singletonList(customerUpdateMsg)) + .build(); + } + break; + case DELETED: + CustomerUpdateMsg customerUpdateMsg = + ctx.getCustomerMsgConstructor().constructCustomerDeleteMsg(customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllCustomerUpdateMsg(Collections.singletonList(customerUpdateMsg)) + .build(); + break; + } + log.trace("[{}] customer processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processRuleChain(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + RuleChain ruleChain = ctx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + if (ruleChain != null) { + RuleChainUpdateMsg ruleChainUpdateMsg = + ctx.getRuleChainMsgConstructor().constructRuleChainUpdatedMsg(edge.getRootRuleChainId(), msgType, ruleChain); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainUpdateMsg(Collections.singletonList(ruleChainUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainUpdateMsg(Collections.singletonList(ctx.getRuleChainMsgConstructor().constructRuleChainDeleteMsg(ruleChainId))) + .build(); + break; + } + log.trace("[{}] rule chain processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processRuleChainMetadata(EdgeEvent edgeEvent, UpdateMsgType msgType) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + RuleChain ruleChain = ctx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + DownlinkMsg downlinkMsg = null; + if (ruleChain != null) { + RuleChainMetaData ruleChainMetaData = ctx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + ctx.getRuleChainMsgConstructor().constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + if (ruleChainMetadataUpdateMsg != null) { + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainMetadataUpdateMsg(Collections.singletonList(ruleChainMetadataUpdateMsg)) + .build(); + } + } + log.trace("[{}] rule chain metadata processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processUser(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + UserId userId = new UserId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + User user = ctx.getUserService().findUserById(edgeEvent.getTenantId(), userId); + if (user != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(user); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserUpdateMsg(Collections.singletonList(ctx.getUserMsgConstructor().constructUserUpdatedMsg(msgType, user, customerId))) + .build(); + } + break; + case DELETED: + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserUpdateMsg(Collections.singletonList(ctx.getUserMsgConstructor().constructUserDeleteMsg(userId))) + .build(); + break; + case CREDENTIALS_UPDATED: + UserCredentials userCredentialsByUserId = ctx.getUserService().findUserCredentialsByUserId(edge.getTenantId(), userId); + if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + UserCredentialsUpdateMsg userCredentialsUpdateMsg = + ctx.getUserMsgConstructor().constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserCredentialsUpdateMsg(Collections.singletonList(userCredentialsUpdateMsg)) + .build(); + } + } + log.trace("[{}] user processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private CustomerId getCustomerIdIfEdgeAssignedToCustomer(HasCustomerId hasCustomerIdEntity) { + if (!edge.getCustomerId().isNullUid() && edge.getCustomerId().equals(hasCustomerIdEntity.getCustomerId())) { + return edge.getCustomerId(); + } else { + return null; + } + } + + private DownlinkMsg processRelation(EdgeEvent edgeEvent, UpdateMsgType msgType) { + EntityRelation entityRelation = mapper.convertValue(edgeEvent.getBody(), EntityRelation.class); + RelationUpdateMsg r = ctx.getRelationMsgConstructor().constructRelationUpdatedMsg(msgType, entityRelation); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllRelationUpdateMsg(Collections.singletonList(r)) + .build(); + log.trace("[{}] relation processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAlarm(EdgeEvent edgeEvent, UpdateMsgType msgType) { + DownlinkMsg downlinkMsg = null; + try { + AlarmId alarmId = new AlarmId(edgeEvent.getEntityId()); + Alarm alarm = ctx.getAlarmService().findAlarmByIdAsync(edgeEvent.getTenantId(), alarmId).get(); + if (alarm != null) { + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAlarmUpdateMsg(Collections.singletonList(ctx.getAlarmMsgConstructor().constructAlarmUpdatedMsg(edge.getTenantId(), msgType, alarm))) + .build(); + } + } catch (Exception e) { + log.error("Can't process alarm msg [{}] [{}]", edgeEvent, msgType, e); + } + log.trace("[{}] alarm processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processWidgetsBundle(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + WidgetsBundle widgetsBundle = ctx.getWidgetsBundleService().findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); + if (widgetsBundle != null) { + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + ctx.getWidgetsBundleMsgConstructor().constructWidgetsBundleUpdateMsg(msgType, widgetsBundle); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetsBundleUpdateMsg(Collections.singletonList(widgetsBundleUpdateMsg)) + .build(); + } + break; + case DELETED: + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + ctx.getWidgetsBundleMsgConstructor().constructWidgetsBundleDeleteMsg(widgetsBundleId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetsBundleUpdateMsg(Collections.singletonList(widgetsBundleUpdateMsg)) + .build(); + break; + } + log.trace("[{}] widget bundle processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processWidgetType(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + WidgetType widgetType = ctx.getWidgetTypeService().findWidgetTypeById(edgeEvent.getTenantId(), widgetTypeId); + if (widgetType != null) { + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + ctx.getWidgetTypeMsgConstructor().constructWidgetTypeUpdateMsg(msgType, widgetType); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetTypeUpdateMsg(Collections.singletonList(widgetTypeUpdateMsg)) + .build(); + } + break; + case DELETED: + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + ctx.getWidgetTypeMsgConstructor().constructWidgetTypeDeleteMsg(widgetTypeId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetTypeUpdateMsg(Collections.singletonList(widgetTypeUpdateMsg)) + .build(); + break; + } + log.trace("[{}] widget type processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAdminSettings(EdgeEvent edgeEvent) { + AdminSettings adminSettings = mapper.convertValue(edgeEvent.getBody(), AdminSettings.class); + AdminSettingsUpdateMsg t = ctx.getAdminSettingsMsgConstructor().constructAdminSettingsUpdateMsg(adminSettings); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllAdminSettingsUpdateMsg(Collections.singletonList(t)) + .build(); + log.trace("[{}] admin settings processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private UpdateMsgType getResponseMsgType(EdgeEventActionType actionType) { + switch (actionType) { + case UPDATED: + case CREDENTIALS_UPDATED: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + return UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE; + case ADDED: + case ASSIGNED_TO_EDGE: + case RELATION_ADD_OR_UPDATE: + return UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; + case DELETED: + case UNASSIGNED_FROM_EDGE: + case RELATION_DELETED: + return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + case ALARM_ACK: + return UpdateMsgType.ALARM_ACK_RPC_MESSAGE; + case ALARM_CLEAR: + return UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE; + default: + throw new RuntimeException("Unsupported actionType [" + actionType + "]"); + } + } + + private DownlinkMsg constructEntityDataProtoMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { + EntityDataProto entityDataProto = ctx.getEntityDataMsgConstructor().constructEntityDataMsg(entityId, actionType, entityData); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityData(Collections.singletonList(entityDataProto)) + .build(); + log.trace("[{}] entity data proto processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { + List> result = new ArrayList<>(); + try { + if (uplinkMsg.getEntityDataCount() > 0) { + for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { + result.addAll(ctx.getTelemetryProcessor().onTelemetryUpdate(edge.getTenantId(), entityData)); + } + } + if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().onDeviceUpdate(edge.getTenantId(), edge, deviceUpdateMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().onDeviceCredentialsUpdate(edge.getTenantId(), deviceCredentialsUpdateMsg)); + } + } + if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { + for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { + result.add(ctx.getAlarmProcessor().onAlarmUpdate(edge.getTenantId(), alarmUpdateMsg)); + } + } + if (uplinkMsg.getRelationUpdateMsgCount() > 0) { + for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { + result.add(ctx.getRelationProcessor().onRelationUpdate(edge.getTenantId(), relationUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { + for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); + } + } + if (uplinkMsg.getAttributesRequestMsgCount() > 0) { + for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); + } + } + if (uplinkMsg.getRelationRequestMsgCount() > 0) { + for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); + } + } + if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { + for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { + for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceRpcCallResponseMsg(edge.getTenantId(), deviceRpcCallMsg)); + } + } + } catch (Exception e) { + log.error("[{}] Can't process uplink msg [{}]", this.sessionId, uplinkMsg, e); + } + return Futures.allAsList(result); + } + + private ConnectResponseMsg processConnect(ConnectRequestMsg request) { + log.trace("[{}] processConnect [{}]", this.sessionId, request); + Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); + if (optional.isPresent()) { + edge = optional.get(); + try { + if (edge.getSecret().equals(request.getEdgeSecret())) { + sessionOpenListener.accept(edge.getId(), this); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.ACCEPTED) + .setErrorMsg("") + .setConfiguration(constructEdgeConfigProto(edge)).build(); + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to validate the edge!") + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } catch (Exception e) { + log.error("[{}] Failed to process edge connection!", request.getEdgeRoutingKey(), e); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) + .setErrorMsg("Failed to process edge connection!") + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + + private EdgeConfiguration constructEdgeConfigProto(Edge edge) { + EdgeConfiguration.Builder builder = EdgeConfiguration.newBuilder() + .setEdgeIdMSB(edge.getId().getId().getMostSignificantBits()) + .setEdgeIdLSB(edge.getId().getId().getLeastSignificantBits()) + .setTenantIdMSB(edge.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(edge.getTenantId().getId().getLeastSignificantBits()) + .setName(edge.getName()) + .setType(edge.getType()) + .setRoutingKey(edge.getRoutingKey()) + .setSecret(edge.getSecret()) + .setEdgeLicenseKey(edge.getEdgeLicenseKey()) + .setCloudEndpoint(edge.getCloudEndpoint()) + .setAdditionalInfo(JacksonUtil.toString(edge.getAdditionalInfo())) + .setCloudType("CE"); + if (edge.getCustomerId() != null) { + builder.setCustomerIdMSB(edge.getCustomerId().getId().getMostSignificantBits()) + .setCustomerIdLSB(edge.getCustomerId().getId().getLeastSignificantBits()); + } + return builder + .build(); + } + + @Override + public void close() { + log.debug("[{}] Closing session", sessionId); + connected = false; + try { + outputStream.onCompleted(); + } catch (Exception e) { + log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage()); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java new file mode 100644 index 0000000000..ddea009849 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; + +public interface EdgeRpcService { + + void updateEdge(Edge edge); + + void deleteEdge(EdgeId edgeId); + + void onEdgeEvent(EdgeId edgeId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java new file mode 100644 index 0000000000..891b42f815 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AdminSettingsUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AdminSettingsMsgConstructor { + + public AdminSettingsUpdateMsg constructAdminSettingsUpdateMsg(AdminSettings adminSettings) { + AdminSettingsUpdateMsg.Builder builder = AdminSettingsUpdateMsg.newBuilder() + .setKey(adminSettings.getKey()) + .setJsonValue(JacksonUtil.toString(adminSettings.getJsonValue())); + if (adminSettings.getId() != null) { + builder.setIsSystem(true); + } + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java new file mode 100644 index 0000000000..9d471b76ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AlarmMsgConstructor { + + @Autowired + private DeviceService deviceService; + + @Autowired + private AssetService assetService; + + @Autowired + private EntityViewService entityViewService; + + public AlarmUpdateMsg constructAlarmUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, Alarm alarm) { + String entityName = null; + switch (alarm.getOriginator().getEntityType()) { + case DEVICE: + entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName(); + break; + case ASSET: + entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName(); + break; + case ENTITY_VIEW: + entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName(); + break; + } + AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() + .setMsgType(msgType) + .setName(alarm.getName()) + .setType(alarm.getType()) + .setOriginatorName(entityName) + .setOriginatorType(alarm.getOriginator().getEntityType().name()) + .setSeverity(alarm.getSeverity().name()) + .setStatus(alarm.getStatus().name()) + .setStartTs(alarm.getStartTs()) + .setEndTs(alarm.getEndTs()) + .setAckTs(alarm.getAckTs()) + .setClearTs(alarm.getClearTs()) + .setDetails(JacksonUtil.toString(alarm.getDetails())) + .setPropagate(alarm.isPropagate()); + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java new file mode 100644 index 0000000000..f78956b4f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +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.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AssetMsgConstructor { + + public AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset, CustomerId customerId) { + AssetUpdateMsg.Builder builder = AssetUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(asset.getId().getId().getMostSignificantBits()) + .setIdLSB(asset.getId().getId().getLeastSignificantBits()) + .setName(asset.getName()) + .setType(asset.getType()); + if (asset.getLabel() != null) { + builder.setLabel(asset.getLabel()); + } + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (asset.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); + } + return builder.build(); + } + + public AssetUpdateMsg constructAssetDeleteMsg(AssetId assetId) { + return AssetUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(assetId.getId().getMostSignificantBits()) + .setIdLSB(assetId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java new file mode 100644 index 0000000000..5c0ec9ff4b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class CustomerMsgConstructor { + + public CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { + CustomerUpdateMsg.Builder builder = CustomerUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(customer.getId().getId().getMostSignificantBits()) + .setIdLSB(customer.getId().getId().getLeastSignificantBits()) + .setTitle(customer.getTitle()); + if (customer.getCountry() != null) { + builder.setCountry(customer.getCountry()); + } + if (customer.getState() != null) { + builder.setState(customer.getState()); + } + if (customer.getCity() != null) { + builder.setCity(customer.getCity()); + } + if (customer.getAddress() != null) { + builder.setAddress(customer.getAddress()); + } + if (customer.getAddress2() != null) { + builder.setAddress2(customer.getAddress2()); + } + if (customer.getZip() != null) { + builder.setZip(customer.getZip()); + } + if (customer.getPhone() != null) { + builder.setPhone(customer.getPhone()); + } + if (customer.getEmail() != null) { + builder.setEmail(customer.getEmail()); + } + if (customer.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(customer.getAdditionalInfo())); + } + return builder.build(); + } + + public CustomerUpdateMsg constructCustomerDeleteMsg(CustomerId customerId) { + return CustomerUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(customerId.getId().getMostSignificantBits()) + .setIdLSB(customerId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java new file mode 100644 index 0000000000..a550d52395 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DashboardMsgConstructor { + + public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard, CustomerId customerId) { + DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) + .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()) + .setTitle(dashboard.getTitle()) + .setConfiguration(JacksonUtil.toString(dashboard.getConfiguration())); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + return builder.build(); + } + + public DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId) { + return DashboardUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(dashboardId.getId().getMostSignificantBits()) + .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java new file mode 100644 index 0000000000..df7bfc9e1a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.RpcRequestMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.UUID; + +@Component +@TbCoreComponent +public class DeviceMsgConstructor { + + protected static final ObjectMapper mapper = new ObjectMapper(); + + public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device, CustomerId customerId, String conflictName) { + DeviceUpdateMsg.Builder builder = DeviceUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(device.getId().getId().getMostSignificantBits()) + .setIdLSB(device.getId().getId().getLeastSignificantBits()) + .setName(device.getName()) + .setType(device.getType()); + if (device.getLabel() != null) { + builder.setLabel(device.getLabel()); + } + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (device.getDeviceProfileId() != null) { + builder.setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits()); + builder.setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()); + } + if (device.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo())); + } + if (conflictName != null) { + builder.setConflictName(conflictName); + } + return builder.build(); + } + + public DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { + DeviceCredentialsUpdateMsg.Builder builder = DeviceCredentialsUpdateMsg.newBuilder() + .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits()); + if (deviceCredentials.getCredentialsType() != null) { + builder.setCredentialsType(deviceCredentials.getCredentialsType().name()) + .setCredentialsId(deviceCredentials.getCredentialsId()); + } + if (deviceCredentials.getCredentialsValue() != null) { + builder.setCredentialsValue(deviceCredentials.getCredentialsValue()); + } + return builder.build(); + } + + public DeviceUpdateMsg constructDeviceDeleteMsg(DeviceId deviceId) { + return DeviceUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceId.getId().getMostSignificantBits()) + .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); + } + + public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { + int requestId = body.get("requestId").asInt(); + boolean oneway = body.get("oneway").asBoolean(); + UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); + long expirationTime = body.get("expirationTime").asLong(); + String method = body.get("method").asText(); + String params = body.get("params").asText(); + + RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); + requestBuilder.setMethod(method); + requestBuilder.setParams(params); + DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() + .setDeviceIdMSB(deviceId.getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getLeastSignificantBits()) + .setRequestUuidMSB(requestUUID.getMostSignificantBits()) + .setRequestUuidLSB(requestUUID.getLeastSignificantBits()) + .setRequestId(requestId) + .setExpirationTime(expirationTime) + .setOneway(oneway) + .setRequestMsg(requestBuilder.build()); + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java new file mode 100644 index 0000000000..316707b9ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DeviceProfileMsgConstructor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { + DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + .setName(deviceProfile.getName()) + .setDefault(deviceProfile.isDefault()) + .setType(deviceProfile.getType().name()) + .setTransportType(deviceProfile.getTransportType().name()) + .setProvisionType(deviceProfile.getProvisionType().name()) + .setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile.getProfileData()))); + // TODO: voba - should this be always null at the moment?? +// if (deviceProfile.getDefaultRuleChainId() != null) { +// builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultRuleChainId().getId().getMostSignificantBits()) +// .setDefaultRuleChainIdLSB(deviceProfile.getDefaultRuleChainId().getId().getLeastSignificantBits()); +// } +// if (deviceProfile.getDefaultQueueName() != null) { +// builder.setDefaultQueueName(deviceProfile.getDefaultQueueName()); +// } + if (deviceProfile.getDescription() != null) { + builder.setDescription(deviceProfile.getDescription()); + } + if (deviceProfile.getProvisionDeviceKey() != null) { + builder.setProvisionDeviceKey(deviceProfile.getProvisionDeviceKey()); + } + return builder.build(); + } + + public DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { + return DeviceProfileUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceProfileId.getId().getMostSignificantBits()) + .setIdLSB(deviceProfileId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java new file mode 100644 index 0000000000..9b204d3187 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.List; + +@Component +@Slf4j +@TbCoreComponent +public class EntityDataMsgConstructor { + + public EntityDataProto constructEntityDataMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { + EntityDataProto.Builder builder = EntityDataProto.newBuilder() + .setEntityIdMSB(entityId.getId().getMostSignificantBits()) + .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) + .setEntityType(entityId.getEntityType().name()); + switch (actionType) { + case TIMESERIES_UPDATED: + try { + JsonObject data = entityData.getAsJsonObject(); + long ts; + if (data.get("ts") != null && !data.get("ts").isJsonNull()) { + ts = data.getAsJsonPrimitive("ts").getAsLong(); + } else { + ts = System.currentTimeMillis(); + } + builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); + } catch (Exception e) { + log.warn("[{}] Can't convert to telemetry proto, entityData [{}]", entityId, entityData, e); + } + break; + case ATTRIBUTES_UPDATED: + try { + JsonObject data = entityData.getAsJsonObject(); + TransportProtos.PostAttributeMsg attributesUpdatedMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); + builder.setAttributesUpdatedMsg(attributesUpdatedMsg); + builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + } catch (Exception e) { + log.warn("[{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", entityId, entityData, e); + } + break; + case POST_ATTRIBUTES: + try { + JsonObject data = entityData.getAsJsonObject(); + TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); + builder.setPostAttributesMsg(postAttributesMsg); + builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + } catch (Exception e) { + log.warn("[{}] Can't convert to PostAttributesMsg, entityData [{}]", entityId, entityData, e); + } + break; + case ATTRIBUTES_DELETED: + try { + AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder(); + attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString()); + JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys"); + List keys = new Gson().fromJson(jsonArray.toString(), List.class); + attributeDeleteMsg.addAllAttributeNames(keys); + attributeDeleteMsg.build(); + builder.setAttributeDeleteMsg(attributeDeleteMsg); + } catch (Exception e) { + log.warn("[{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", entityId, entityData, e); + } + break; + } + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java new file mode 100644 index 0000000000..0f1e3d9e49 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.EdgeEntityType; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class EntityViewMsgConstructor { + + public EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView, CustomerId customerId) { + EdgeEntityType entityType; + switch (entityView.getEntityId().getEntityType()) { + case DEVICE: + entityType = EdgeEntityType.DEVICE; + break; + case ASSET: + entityType = EdgeEntityType.ASSET; + break; + default: + throw new RuntimeException("Unsupported entity type [" + entityView.getEntityId().getEntityType() + "]"); + } + EntityViewUpdateMsg.Builder builder = EntityViewUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(entityView.getId().getId().getMostSignificantBits()) + .setIdLSB(entityView.getId().getId().getLeastSignificantBits()) + .setName(entityView.getName()) + .setType(entityView.getType()) + .setEntityIdMSB(entityView.getEntityId().getId().getMostSignificantBits()) + .setEntityIdLSB(entityView.getEntityId().getId().getLeastSignificantBits()) + .setEntityType(entityType); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (entityView.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(entityView.getAdditionalInfo())); + } + return builder.build(); + } + + public EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId) { + return EntityViewUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(entityViewId.getId().getMostSignificantBits()) + .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java new file mode 100644 index 0000000000..c805085b6b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class RelationMsgConstructor { + + public RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { + RelationUpdateMsg.Builder builder = RelationUpdateMsg.newBuilder() + .setMsgType(msgType) + .setFromIdMSB(entityRelation.getFrom().getId().getMostSignificantBits()) + .setFromIdLSB(entityRelation.getFrom().getId().getLeastSignificantBits()) + .setFromEntityType(entityRelation.getFrom().getEntityType().name()) + .setToIdMSB(entityRelation.getTo().getId().getMostSignificantBits()) + .setToIdLSB(entityRelation.getTo().getId().getLeastSignificantBits()) + .setToEntityType(entityRelation.getTo().getEntityType().name()) + .setType(entityRelation.getType()) + .setAdditionalInfo(JacksonUtil.toString(entityRelation.getAdditionalInfo())); + if (entityRelation.getTypeGroup() != null) { + builder.setTypeGroup(entityRelation.getTypeGroup().name()); + } + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java new file mode 100644 index 0000000000..c1bd3d816b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.NodeConnectionInfoProto; +import org.thingsboard.server.gen.edge.RuleChainConnectionInfoProto; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.RuleNodeProto; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +@TbCoreComponent +public class RuleChainMsgConstructor { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public RuleChainUpdateMsg constructRuleChainUpdatedMsg(RuleChainId edgeRootRuleChainId, UpdateMsgType msgType, RuleChain ruleChain) { + RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) + .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()) + .setName(ruleChain.getName()) + .setRoot(ruleChain.getId().equals(edgeRootRuleChainId)) + .setDebugMode(ruleChain.isDebugMode()) + .setConfiguration(JacksonUtil.toString(ruleChain.getConfiguration())); + if (ruleChain.getFirstRuleNodeId() != null) { + builder.setFirstRuleNodeIdMSB(ruleChain.getFirstRuleNodeId().getId().getMostSignificantBits()) + .setFirstRuleNodeIdLSB(ruleChain.getFirstRuleNodeId().getId().getLeastSignificantBits()); + } + return builder.build(); + } + + public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { + try { + RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder() + .setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()) + .addAllNodes(constructNodes(ruleChainMetaData.getNodes())) + .addAllConnections(constructConnections(ruleChainMetaData.getConnections())) + .addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections())); + if (ruleChainMetaData.getFirstNodeIndex() != null) { + builder.setFirstNodeIndex(ruleChainMetaData.getFirstNodeIndex()); + } else { + builder.setFirstNodeIndex(-1); + } + builder.setMsgType(msgType); + return builder.build(); + } catch (JsonProcessingException ex) { + log.error("Can't construct RuleChainMetadataUpdateMsg", ex); + } + return null; + } + + private List constructConnections(List connections) { + List result = new ArrayList<>(); + if (connections != null && !connections.isEmpty()) { + for (NodeConnectionInfo connection : connections) { + result.add(constructConnection(connection)); + } + } + return result; + } + + private NodeConnectionInfoProto constructConnection(NodeConnectionInfo connection) { + return NodeConnectionInfoProto.newBuilder() + .setFromIndex(connection.getFromIndex()) + .setToIndex(connection.getToIndex()) + .setType(connection.getType()) + .build(); + } + + private List constructNodes(List nodes) throws JsonProcessingException { + List result = new ArrayList<>(); + if (nodes != null && !nodes.isEmpty()) { + for (RuleNode node : nodes) { + result.add(constructNode(node)); + } + } + return result; + } + + private List constructRuleChainConnections(List ruleChainConnections) throws JsonProcessingException { + List result = new ArrayList<>(); + if (ruleChainConnections != null && !ruleChainConnections.isEmpty()) { + for (RuleChainConnectionInfo ruleChainConnectionInfo : ruleChainConnections) { + result.add(constructRuleChainConnection(ruleChainConnectionInfo)); + } + } + return result; + } + + private RuleChainConnectionInfoProto constructRuleChainConnection(RuleChainConnectionInfo ruleChainConnectionInfo) throws JsonProcessingException { + return RuleChainConnectionInfoProto.newBuilder() + .setFromIndex(ruleChainConnectionInfo.getFromIndex()) + .setTargetRuleChainIdMSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getMostSignificantBits()) + .setTargetRuleChainIdLSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getLeastSignificantBits()) + .setType(ruleChainConnectionInfo.getType()) + .setAdditionalInfo(objectMapper.writeValueAsString(ruleChainConnectionInfo.getAdditionalInfo())) + .build(); + } + + private RuleNodeProto constructNode(RuleNode node) throws JsonProcessingException { + return RuleNodeProto.newBuilder() + .setIdMSB(node.getId().getId().getMostSignificantBits()) + .setIdLSB(node.getId().getId().getLeastSignificantBits()) + .setType(node.getType()) + .setName(node.getName()) + .setDebugMode(node.isDebugMode()) + .setConfiguration(objectMapper.writeValueAsString(node.getConfiguration())) + .setAdditionalInfo(objectMapper.writeValueAsString(node.getAdditionalInfo())) + .build(); + } + + public RuleChainUpdateMsg constructRuleChainDeleteMsg(RuleChainId ruleChainId) { + return RuleChainUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java new file mode 100644 index 0000000000..f69ede9210 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class UserMsgConstructor { + + public UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user, CustomerId customerId) { + UserUpdateMsg.Builder builder = UserUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(user.getId().getId().getMostSignificantBits()) + .setIdLSB(user.getId().getId().getLeastSignificantBits()) + .setEmail(user.getEmail()) + .setAuthority(user.getAuthority().name()); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (user.getFirstName() != null) { + builder.setFirstName(user.getFirstName()); + } + if (user.getLastName() != null) { + builder.setLastName(user.getLastName()); + } + if (user.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(user.getAdditionalInfo())); + } + if (user.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(user.getAdditionalInfo())); + } + return builder.build(); + } + + public UserUpdateMsg constructUserDeleteMsg(UserId userId) { + return UserUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(userId.getId().getMostSignificantBits()) + .setIdLSB(userId.getId().getLeastSignificantBits()).build(); + } + + public UserCredentialsUpdateMsg constructUserCredentialsUpdatedMsg(UserCredentials userCredentials) { + UserCredentialsUpdateMsg.Builder builder = UserCredentialsUpdateMsg.newBuilder() + .setUserIdMSB(userCredentials.getUserId().getId().getMostSignificantBits()) + .setUserIdLSB(userCredentials.getUserId().getId().getLeastSignificantBits()) + .setEnabled(userCredentials.isEnabled()) + .setPassword(userCredentials.getPassword()); + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java new file mode 100644 index 0000000000..672beeda75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class WidgetTypeMsgConstructor { + + public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetType widgetType) { + WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(widgetType.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetType.getId().getId().getLeastSignificantBits()); + if (widgetType.getBundleAlias() != null) { + builder.setBundleAlias(widgetType.getBundleAlias()); + } + if (widgetType.getAlias() != null) { + builder.setAlias(widgetType.getAlias()); + } + if (widgetType.getName() != null) { + builder.setName(widgetType.getName()); + } + if (widgetType.getDescriptor() != null) { + builder.setDescriptorJson(JacksonUtil.toString(widgetType.getDescriptor())); + } + if (widgetType.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + builder.setIsSystem(true); + } + return builder.build(); + } + + public WidgetTypeUpdateMsg constructWidgetTypeDeleteMsg(WidgetTypeId widgetTypeId) { + return WidgetTypeUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetTypeId.getId().getMostSignificantBits()) + .setIdLSB(widgetTypeId.getId().getLeastSignificantBits()) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java new file mode 100644 index 0000000000..fba21ed1ca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.nio.charset.StandardCharsets; + +@Component +@TbCoreComponent +public class WidgetsBundleMsgConstructor { + + public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle) { + WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetsBundle.getId().getId().getLeastSignificantBits()) + .setTitle(widgetsBundle.getTitle()) + .setAlias(widgetsBundle.getAlias()); + if (widgetsBundle.getImage() != null) { + builder.setImage(ByteString.copyFrom(widgetsBundle.getImage().getBytes(StandardCharsets.UTF_8))); + } + if (widgetsBundle.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + builder.setIsSystem(true); + } + return builder.build(); + } + + public WidgetsBundleUpdateMsg constructWidgetsBundleDeleteMsg(WidgetsBundleId widgetsBundleId) { + return WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetsBundleId.getId().getMostSignificantBits()) + .setIdLSB(widgetsBundleId.getId().getLeastSignificantBits()) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java new file mode 100644 index 0000000000..0b9afa4ccf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java @@ -0,0 +1,671 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.init; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AdminSettingsId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@Slf4j +public class DefaultSyncEdgeService implements SyncEdgeService { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private AttributesService attributesService; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private RelationService relationService; + + @Autowired + private DeviceService deviceService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private AssetService assetService; + + @Autowired + private EntityViewService entityViewService; + + @Autowired + private DashboardService dashboardService; + + @Autowired + private UserService userService; + + @Autowired + private WidgetsBundleService widgetsBundleService; + + @Autowired + private WidgetTypeService widgetTypeService; + + @Autowired + private AdminSettingsService adminSettingsService; + + @Autowired + private DbCallbackExecutorService dbCallbackExecutorService; + + @Autowired + private TbClusterService tbClusterService; + + @Override + public void sync(TenantId tenantId, Edge edge) { + log.trace("[{}][{}] Staring edge sync process", tenantId, edge.getId()); + try { + syncWidgetsBundles(tenantId, edge); + // TODO: voba - implement this functionality + // syncAdminSettings(edge); + syncDeviceProfiles(tenantId, edge); + syncRuleChains(tenantId, edge); + syncUsers(tenantId, edge); + syncAssets(tenantId, edge); + syncEntityViews(tenantId, edge); + syncDashboards(tenantId, edge); + syncWidgetsTypes(tenantId, edge); + syncDevices(tenantId, edge); + } catch (Exception e) { + log.error("[{}][{}] Exception during sync process", tenantId, edge.getId(), e); + } + } + + private void syncRuleChains(TenantId tenantId, Edge edge) { + log.trace("[{}] syncRuleChains [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] rule chains(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (RuleChain ruleChain : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.ADDED, ruleChain.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge rule chain(s) on sync!", e); + } + } + + private void syncDevices(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDevices [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] device(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (Device device : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ADDED, device.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge device(s) on sync!", e); + } + } + + private void syncDeviceProfiles(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDeviceProfiles [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (DeviceProfile deviceProfile : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, EdgeEventActionType.ADDED, deviceProfile.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading device profile(s) on sync!", e); + } + } + + private void syncAssets(TenantId tenantId, Edge edge) { + log.trace("[{}] syncAssets [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] asset(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (Asset asset : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.ADDED, asset.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge asset(s) on sync!", e); + } + } + + private void syncEntityViews(TenantId tenantId, Edge edge) { + log.trace("[{}] syncEntityViews [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] entity view(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (EntityView entityView : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ENTITY_VIEW, EdgeEventActionType.ADDED, entityView.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge entity view(s) on sync!", e); + } + } + + private void syncDashboards(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDashboards [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] dashboard(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (DashboardInfo dashboardInfo : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DASHBOARD, EdgeEventActionType.ADDED, dashboardInfo.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge dashboard(s) on sync!", e); + } + } + + private void syncUsers(TenantId tenantId, Edge edge) { + log.trace("[{}] syncUsers [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findTenantAdmins(tenantId, pageLink); + pushUsersToEdge(tenantId, pageData, edge); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + syncCustomerUsers(tenantId, edge); + } catch (Exception e) { + log.error("Exception during loading edge user(s) on sync!", e); + } + } + + private void syncCustomerUsers(TenantId tenantId, Edge edge) { + if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, edge.getCustomerId(), null); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findCustomerUsers(tenantId, edge.getCustomerId(), pageLink); + pushUsersToEdge(tenantId, pageData, edge); + if (pageData != null && pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData != null && pageData.hasNext()); + } + } + + private void pushUsersToEdge(TenantId tenantId, PageData pageData, Edge edge) { + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (User user : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null); + } + } + } + + private void syncWidgetsBundles(TenantId tenantId, Edge edge) { + log.trace("[{}] syncWidgetsBundles [{}]", tenantId, edge.getName()); + List widgetsBundlesToPush = new ArrayList<>(); + widgetsBundlesToPush.addAll(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); + widgetsBundlesToPush.addAll(widgetsBundleService.findSystemWidgetsBundles(tenantId)); + try { + for (WidgetsBundle widgetsBundle : widgetsBundlesToPush) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGETS_BUNDLE, EdgeEventActionType.ADDED, widgetsBundle.getId(), null); + } + } catch (Exception e) { + log.error("Exception during loading widgets bundle(s) on sync!", e); + } + } + + private void syncWidgetsTypes(TenantId tenantId, Edge edge) { + log.trace("[{}] syncWidgetsTypes [{}]", tenantId, edge.getName()); + List widgetsBundlesToPush = new ArrayList<>(); + widgetsBundlesToPush.addAll(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); + widgetsBundlesToPush.addAll(widgetsBundleService.findSystemWidgetsBundles(tenantId)); + try { + for (WidgetsBundle widgetsBundle : widgetsBundlesToPush) { + List widgetTypesToPush = + widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(widgetsBundle.getTenantId(), widgetsBundle.getAlias()); + for (WidgetType widgetType : widgetTypesToPush) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGET_TYPE, EdgeEventActionType.ADDED, widgetType.getId(), null); + } + } + } catch (Exception e) { + log.error("Exception during loading widgets type(s) on sync!", e); + } + } + + private void syncAdminSettings(TenantId tenantId, Edge edge) { + log.trace("[{}] syncAdminSettings [{}]", tenantId, edge.getName()); + try { + AdminSettings systemMailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailSettings)); + AdminSettings tenantMailSettings = convertToTenantAdminSettings(systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailSettings)); + AdminSettings systemMailTemplates = loadMailTemplates(); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailTemplates)); + AdminSettings tenantMailTemplates = convertToTenantAdminSettings(systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailTemplates)); + } catch (Exception e) { + log.error("Can't load admin settings", e); + } + } + + private AdminSettings loadMailTemplates() throws Exception { + Map mailTemplates = new HashMap<>(); + Pattern startPattern = Pattern.compile("
"); + Pattern endPattern = Pattern.compile("
"); + File[] files = new DefaultResourceLoader().getResource("classpath:/templates/").getFile().listFiles(); + for (File file : files) { + Map mailTemplate = new HashMap<>(); + String name = validateName(file.getName()); + String stringTemplate = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + Matcher start = startPattern.matcher(stringTemplate); + Matcher end = endPattern.matcher(stringTemplate); + if (start.find() && end.find()) { + String body = StringUtils.substringBetween(stringTemplate, start.group(), end.group()).replaceAll("\t", ""); + String subject = StringUtils.substringBetween(body, "

", "

"); + mailTemplate.put("subject", subject); + mailTemplate.put("body", body); + mailTemplates.put(name, mailTemplate); + } else { + log.error("Can't load mail template from file {}", file.getName()); + } + } + AdminSettings adminSettings = new AdminSettings(); + adminSettings.setId(new AdminSettingsId(Uuids.timeBased())); + adminSettings.setKey("mailTemplates"); + adminSettings.setJsonValue(mapper.convertValue(mailTemplates, JsonNode.class)); + return adminSettings; + } + + private String validateName(String name) throws Exception { + StringBuilder nameBuilder = new StringBuilder(); + name = name.replace(".vm", ""); + String[] nameParts = name.split("\\."); + if (nameParts.length >= 1) { + nameBuilder.append(nameParts[0]); + for (int i = 1; i < nameParts.length; i++) { + String word = WordUtils.capitalize(nameParts[i]); + nameBuilder.append(word); + } + return nameBuilder.toString(); + } else { + throw new Exception("Error during filename validation"); + } + } + + private AdminSettings convertToTenantAdminSettings(String key, ObjectNode jsonValue) { + AdminSettings tenantMailSettings = new AdminSettings(); + jsonValue.put("useSystemMailSettings", true); + tenantMailSettings.setJsonValue(jsonValue); + tenantMailSettings.setKey(key); + return tenantMailSettings; + } + + @Override + public ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg) { + log.trace("[{}] processRuleChainMetadataRequestMsg [{}][{}]", tenantId, edge.getName(), ruleChainMetadataRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (ruleChainMetadataRequestMsg.getRuleChainIdMSB() != 0 && ruleChainMetadataRequestMsg.getRuleChainIdLSB() != 0) { + RuleChainId ruleChainId = + new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", ruleChainMetadataRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + @Override + public ListenableFuture processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg) { + log.trace("[{}] processAttributesRequestMsg [{}][{}]", tenantId, edge.getName(), attributesRequestMsg); + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(attributesRequestMsg.getEntityType()), + new UUID(attributesRequestMsg.getEntityIdMSB(), attributesRequestMsg.getEntityIdLSB())); + final EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); + if (type != null) { + SettableFuture futureToSet = SettableFuture.create(); + String scope = attributesRequestMsg.getScope(); + ListenableFuture> ssAttrFuture = attributesService.findAll(tenantId, entityId, scope); + Futures.addCallback(ssAttrFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List ssAttributes) { + if (ssAttributes != null && !ssAttributes.isEmpty()) { + try { + Map entityData = new HashMap<>(); + ObjectNode attributes = mapper.createObjectNode(); + for (AttributeKvEntry attr : ssAttributes) { + if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { + attributes.put(attr.getKey(), attr.getBooleanValue().get()); + } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { + attributes.put(attr.getKey(), attr.getDoubleValue().get()); + } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { + attributes.put(attr.getKey(), attr.getLongValue().get()); + } else { + attributes.put(attr.getKey(), attr.getValueAsString()); + } + } + entityData.put("kv", attributes); + entityData.put("scope", scope); + JsonNode body = mapper.valueToTree(entityData); + log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); + saveEdgeEvent(tenantId, + edge.getId(), + type, + EdgeEventActionType.ATTRIBUTES_UPDATED, + entityId, + body); + } catch (Exception e) { + log.error("[{}] Failed to send attribute updates to the edge", edge.getName(), e); + throw new RuntimeException("[" + edge.getName() + "] Failed to send attribute updates to the edge", e); + } + } else { + log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, + edge.getName(), + entityId.getEntityType(), + entityId.getId()); + } + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save attributes [{}]", attributesRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } else { + log.warn("[{}] Type doesn't supported {}", tenantId, entityId.getEntityType()); + return Futures.immediateFuture(null); + } + } + + @Override + public ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg) { + log.trace("[{}] processRelationRequestMsg [{}][{}]", tenantId, edge.getName(), relationRequestMsg); + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(relationRequestMsg.getEntityType()), + new UUID(relationRequestMsg.getEntityIdMSB(), relationRequestMsg.getEntityIdLSB())); + + List>> futures = new ArrayList<>(); + futures.add(findRelationByQuery(tenantId, edge, entityId, EntitySearchDirection.FROM)); + futures.add(findRelationByQuery(tenantId, edge, entityId, EntitySearchDirection.TO)); + ListenableFuture>> relationsListFuture = Futures.allAsList(futures); + SettableFuture futureToSet = SettableFuture.create(); + Futures.addCallback(relationsListFuture, new FutureCallback>>() { + @Override + public void onSuccess(@Nullable List> relationsList) { + try { + if (relationsList != null && !relationsList.isEmpty()) { + for (List entityRelations : relationsList) { + log.trace("[{}] [{}] [{}] relation(s) are going to be pushed to edge.", edge.getId(), entityId, entityRelations.size()); + for (EntityRelation relation : entityRelations) { + try { + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + saveEdgeEvent(tenantId, + edge.getId(), + EdgeEventType.RELATION, + EdgeEventActionType.ADDED, + null, + mapper.valueToTree(relation)); + } + } catch (Exception e) { + log.error("Exception during loading relation [{}] to edge on sync!", relation, e); + futureToSet.setException(e); + return; + } + } + } + } + futureToSet.set(null); + } catch (Exception e) { + log.error("Exception during loading relation(s) to edge on sync!", e); + futureToSet.setException(e); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Can't find relation by query. Entity id [{}]", tenantId, entityId, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } + + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(entityId, direction, -1, false)); + return relationService.findByQuery(tenantId, query); + } + + @Override + public ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg) { + log.trace("[{}] processDeviceCredentialsRequestMsg [{}][{}]", tenantId, edge.getName(), deviceCredentialsRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (deviceCredentialsRequestMsg.getDeviceIdMSB() != 0 && deviceCredentialsRequestMsg.getDeviceIdLSB() != 0) { + DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsRequestMsg.getDeviceIdMSB(), deviceCredentialsRequestMsg.getDeviceIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_UPDATED, deviceId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", deviceCredentialsRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + @Override + public ListenableFuture processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg) { + log.trace("[{}] processUserCredentialsRequestMsg [{}][{}]", tenantId, edge.getName(), userCredentialsRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (userCredentialsRequestMsg.getUserIdMSB() != 0 && userCredentialsRequestMsg.getUserIdLSB() != 0) { + UserId userId = new UserId(new UUID(userCredentialsRequestMsg.getUserIdMSB(), userCredentialsRequestMsg.getUserIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.CREDENTIALS_UPDATED, userId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", userCredentialsRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + private ListenableFuture saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.trace("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + tbClusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + return future; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java new file mode 100644 index 0000000000..e95809d019 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.init; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; + +public interface SyncEdgeService { + + void sync(TenantId tenantId, Edge edge); + + ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg); + + ListenableFuture processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg); + + ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + + ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); + + ListenableFuture processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java new file mode 100644 index 0000000000..5e7e1e5844 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +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.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@Slf4j +@TbCoreComponent +public class AlarmProcessor extends BaseProcessor { + + public ListenableFuture onAlarmUpdate(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { + log.trace("[{}] onAlarmUpdate [{}]", tenantId, alarmUpdateMsg); + EntityId originatorId = getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), + EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); + if (originatorId == null) { + return Futures.immediateFuture(null); + } + try { + Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); + switch (alarmUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + if (existentAlarm == null || existentAlarm.getStatus().isCleared()) { + existentAlarm = new Alarm(); + existentAlarm.setTenantId(tenantId); + existentAlarm.setType(alarmUpdateMsg.getName()); + existentAlarm.setOriginator(originatorId); + existentAlarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); + existentAlarm.setStartTs(alarmUpdateMsg.getStartTs()); + existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); + existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); + } + existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); + existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); + existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); + existentAlarm.setDetails(mapper.readTree(alarmUpdateMsg.getDetails())); + alarmService.createOrUpdateAlarm(existentAlarm); + break; + case ALARM_ACK_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); + } + break; + case ALARM_CLEAR_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.clearAlarm(tenantId, existentAlarm.getId(), mapper.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); + } + break; + case ENTITY_DELETED_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.deleteAlarm(tenantId, existentAlarm.getId()); + } + break; + } + return Futures.immediateFuture(null); + } catch (Exception e) { + log.error("Failed to process alarm update msg [{}]", alarmUpdateMsg, e); + return Futures.immediateFailedFuture(new RuntimeException("Failed to process alarm update msg", e)); + } + } + + private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { + switch (entityType) { + case DEVICE: + return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); + case ASSET: + return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); + case ENTITY_VIEW: + return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); + default: + return null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java new file mode 100644 index 0000000000..cf7f9e21fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java @@ -0,0 +1,136 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.DefaultTbDeviceProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.state.DeviceStateService; + +@Slf4j +public abstract class BaseProcessor { + + protected static final ObjectMapper mapper = new ObjectMapper(); + + @Autowired + protected AlarmService alarmService; + + @Autowired + protected DeviceService deviceService; + + @Autowired + protected TbDeviceProfileCache deviceProfileCache; + + @Autowired + protected DashboardService dashboardService; + + @Autowired + protected AssetService assetService; + + @Autowired + protected EntityViewService entityViewService; + + @Autowired + protected EdgeService edgeService; + + @Autowired + protected CustomerService customerService; + + @Autowired + protected UserService userService; + + @Autowired + protected RelationService relationService; + + @Autowired + protected DeviceCredentialsService deviceCredentialsService; + + @Autowired + protected AttributesService attributesService; + + @Autowired + protected TbClusterService tbClusterService; + + @Autowired + protected DeviceStateService deviceStateService; + + @Autowired + protected EdgeEventService edgeEventService; + + @Autowired + protected DbCallbackExecutorService dbCallbackExecutorService; + + protected ListenableFuture saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " + + "action [{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + tbClusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + return future; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java new file mode 100644 index 0000000000..35eea3f962 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java @@ -0,0 +1,299 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.RpcError; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +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.model.ModelConstants; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; + +@Component +@Slf4j +@TbCoreComponent +public class DeviceProcessor extends BaseProcessor { + + private static final ReentrantLock deviceCreationLock = new ReentrantLock(); + + public ListenableFuture onDeviceUpdate(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + log.trace("[{}] onDeviceUpdate [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); + switch (deviceUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + String deviceName = deviceUpdateMsg.getName(); + Device device = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (device != null) { + ListenableFuture> future = edgeService.findRelatedEdgeIdsByEntityId(tenantId, device.getId()); + SettableFuture futureToSet = SettableFuture.create(); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + boolean update = false; + if (edgeIds != null && !edgeIds.isEmpty()) { + if (edgeIds.contains(edge.getId())) { + update = true; + } + } + Device device; + if (update) { + log.info("[{}] Device with name '{}' already exists on the cloud, and related to this edge [{}]. " + + "deviceUpdateMsg [{}], Updating device", tenantId, deviceName, edge.getId(), deviceUpdateMsg); + updateDevice(tenantId, edge, deviceUpdateMsg); + } else { + log.info("[{}] Device with name '{}' already exists on the cloud, but not related to this edge [{}]. deviceUpdateMsg [{}]." + + "Creating a new device with random prefix and relate to this edge", tenantId, deviceName, edge.getId(), deviceUpdateMsg); + String newDeviceName = deviceUpdateMsg.getName() + "_" + RandomStringUtils.randomAlphabetic(15); + device = createDevice(tenantId, edge, deviceUpdateMsg, newDeviceName); + ObjectNode body = mapper.createObjectNode(); + body.put("conflictName", deviceName); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ENTITY_MERGE_REQUEST, device.getId(), body); + } + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to get related edge ids by device id [{}], edge [{}]", tenantId, deviceUpdateMsg, edge.getId(), t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } else { + log.info("[{}] Creating new device and replacing device entity on the edge [{}]", tenantId, deviceUpdateMsg); + device = createDevice(tenantId, edge, deviceUpdateMsg, deviceUpdateMsg.getName()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ENTITY_MERGE_REQUEST, device.getId(), null); + } + break; + case ENTITY_UPDATED_RPC_MESSAGE: + updateDevice(tenantId, edge, deviceUpdateMsg); + break; + case ENTITY_DELETED_RPC_MESSAGE: + DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); + Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); + if (deviceToDelete != null) { + deviceService.unassignDeviceFromEdge(tenantId, deviceId, edge.getId()); + } + break; + case UNRECOGNIZED: + log.error("Unsupported msg type {}", deviceUpdateMsg.getMsgType()); + return Futures.immediateFailedFuture(new RuntimeException("Unsupported msg type " + deviceUpdateMsg.getMsgType())); + } + return Futures.immediateFuture(null); + } + + public ListenableFuture onDeviceCredentialsUpdate(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + log.debug("Executing onDeviceCredentialsUpdate, deviceCredentialsUpdateMsg [{}]", deviceCredentialsUpdateMsg); + DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); + ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); + return Futures.transform(deviceFuture, device -> { + if (device != null) { + log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", + device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); + try { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); + deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); + deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.getCredentialsValue()); + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } catch (Exception e) { + log.error("Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", device.getName(), deviceCredentialsUpdateMsg, e); + throw new RuntimeException(e); + } + } + return null; + }, dbCallbackExecutorService); + } + + + private void updateDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + device.setName(deviceUpdateMsg.getName()); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); + if (deviceUpdateMsg.getDeviceProfileIdMSB() != 0 && deviceUpdateMsg.getDeviceProfileIdLSB() != 0) { + DeviceProfileId deviceProfileId = new DeviceProfileId( + new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB())); + device.setDeviceProfileId(deviceProfileId); + } + deviceService.saveDevice(device); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); + } else { + log.warn("[{}] can't find device [{}], edge [{}]", tenantId, deviceUpdateMsg, edge.getId()); + } + } + + private Device createDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg, String deviceName) { + Device device; + try { + deviceCreationLock.lock(); + log.debug("[{}] Creating device entity [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); + device = new Device(); + device.setTenantId(edge.getTenantId()); + // make device private, if edge is public + device.setCustomerId(getCustomerId(edge)); + device.setName(deviceName); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); + if (deviceUpdateMsg.getDeviceProfileIdMSB() != 0 && deviceUpdateMsg.getDeviceProfileIdLSB() != 0) { + DeviceProfileId deviceProfileId = new DeviceProfileId( + new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB())); + device.setDeviceProfileId(deviceProfileId); + } + device = deviceService.saveDevice(device); + createRelationFromEdge(tenantId, edge.getId(), device.getId()); + deviceStateService.onDeviceAdded(device); + pushDeviceCreatedEventToRuleEngine(tenantId, edge, device); + deviceService.assignDeviceToEdge(edge.getTenantId(), device.getId(), edge.getId()); + } finally { + deviceCreationLock.unlock(); + } + return device; + } + + private CustomerId getCustomerId(Edge edge) { + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + return edge.getCustomerId(); + } + Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); + if (publicCustomer.getId().equals(edge.getCustomerId())) { + return null; + } else { + return edge.getCustomerId(); + } + } + + private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(edgeId); + relation.setTo(entityId); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setType(EntityRelation.EDGE_TYPE); + relationService.saveRelation(tenantId, relation); + } + + private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, Device device) { + try { + DeviceId deviceId = device.getId(); + ObjectNode entityNode = mapper.valueToTree(device); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, + getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); + } + + @Override + public void onFailure(Throwable t) { + log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + } + } + + private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { + TbMsgMetaData metaData = getTbMsgMetaData(edge); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + return metaData; + } + + private TbMsgMetaData getTbMsgMetaData(Edge edge) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("edgeId", edge.getId().toString()); + metaData.putValue("edgeName", edge.getName()); + return metaData; + } + + public ListenableFuture processDeviceRpcCallResponseMsg(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { + log.trace("[{}] processDeviceRpcCallResponseMsg [{}]", tenantId, deviceRpcCallMsg); + SettableFuture futureToSet = SettableFuture.create(); + UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); + DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); + + FromDeviceRpcResponse response; + if (!StringUtils.isEmpty(deviceRpcCallMsg.getResponseMsg().getError())) { + response = new FromDeviceRpcResponse(requestUuid, null, RpcError.valueOf(deviceRpcCallMsg.getResponseMsg().getError())); + } else { + response = new FromDeviceRpcResponse(requestUuid, deviceRpcCallMsg.getResponseMsg().getResponse(), null); + } + TbQueueCallback callback = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process push notification to core [{}]", deviceRpcCallMsg, t); + futureToSet.setException(t); + } + }; + FromDeviceRpcResponseActorMsg msg = + new FromDeviceRpcResponseActorMsg(deviceRpcCallMsg.getRequestId(), + tenantId, + deviceId, response); + tbClusterService.pushMsgToCore(msg, callback); + return futureToSet; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java new file mode 100644 index 0000000000..b21a5bd195 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class RelationProcessor extends BaseProcessor { + + public ListenableFuture onRelationUpdate(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { + log.trace("[{}] onRelationUpdate [{}]", tenantId, relationUpdateMsg); + try { + EntityRelation entityRelation = new EntityRelation(); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); + entityRelation.setFrom(fromId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); + entityRelation.setTo(toId); + + entityRelation.setType(relationUpdateMsg.getType()); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup())); + entityRelation.setAdditionalInfo(mapper.readTree(relationUpdateMsg.getAdditionalInfo())); + switch (relationUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + if (isEntityExists(tenantId, entityRelation.getTo()) + && isEntityExists(tenantId, entityRelation.getFrom())) { + relationService.saveRelationAsync(tenantId, entityRelation); + } + break; + case ENTITY_DELETED_RPC_MESSAGE: + relationService.deleteRelation(tenantId, entityRelation); + break; + case UNRECOGNIZED: + log.error("Unsupported msg type"); + } + return Futures.immediateFuture(null); + } catch (Exception e) { + log.error("Failed to process relation update msg [{}]", relationUpdateMsg, e); + return Futures.immediateFailedFuture(new RuntimeException("Failed to process relation update msg", e)); + } + } + + + private boolean isEntityExists(TenantId tenantId, EntityId entityId) throws ThingsboardException { + switch (entityId.getEntityType()) { + case DEVICE: + return deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; + case ASSET: + return assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; + case ENTITY_VIEW: + return entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; + case CUSTOMER: + return customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; + case USER: + return userService.findUserById(tenantId, new UserId(entityId.getId())) != null; + case DASHBOARD: + return dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; + default: + throw new ThingsboardException("Unsupported entity type " + entityId.getEntityType(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java new file mode 100644 index 0000000000..c858955bfc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java @@ -0,0 +1,282 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import 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.Gson; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +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.EntityView; +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.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKey; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class TelemetryProcessor extends BaseProcessor { + + private final Gson gson = new Gson(); + + public List> onTelemetryUpdate(TenantId tenantId, EntityDataProto entityData) { + log.trace("[{}] onTelemetryUpdate [{}]", tenantId, entityData); + List> result = new ArrayList<>(); + EntityId entityId = constructEntityId(entityData); + if ((entityData.hasPostAttributesMsg() || entityData.hasPostTelemetryMsg() || entityData.hasAttributesUpdatedMsg()) && entityId != null) { + // TODO: voba - in terms of performance we should not fetch device from DB by id + // TbMsgMetaData metaData = constructBaseMsgMetadata(tenantId, entityId); + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue(DataConstants.MSG_SOURCE_KEY, DataConstants.EDGE_MSG_SOURCE); + if (entityData.hasPostAttributesMsg()) { + result.add(processPostAttributes(tenantId, entityId, entityData.getPostAttributesMsg(), metaData)); + } + if (entityData.hasAttributesUpdatedMsg()) { + metaData.putValue("scope", entityData.getPostAttributeScope()); + result.add(processAttributesUpdate(tenantId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); + } + if (entityData.hasPostTelemetryMsg()) { + result.add(processPostTelemetry(tenantId, entityId, entityData.getPostTelemetryMsg(), metaData)); + } + } + if (entityData.hasAttributeDeleteMsg()) { + result.add(processAttributeDeleteMsg(tenantId, entityId, entityData.getAttributeDeleteMsg(), entityData.getEntityType())); + } + return result; + } + + private TbMsgMetaData constructBaseMsgMetadata(TenantId tenantId, EntityId entityId) { + TbMsgMetaData metaData = new TbMsgMetaData(); + switch (entityId.getEntityType()) { + case DEVICE: + Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (device != null) { + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + } + break; + case ASSET: + Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); + if (asset != null) { + metaData.putValue("assetName", asset.getName()); + metaData.putValue("assetType", asset.getType()); + } + break; + case ENTITY_VIEW: + EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); + if (entityView != null) { + metaData.putValue("entityViewName", entityView.getName()); + metaData.putValue("entityViewType", entityView.getType()); + } + break; + default: + log.debug("Using empty metadata for entityId [{}]", entityId); + break; + } + return metaData; + } + + private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + if (EntityType.DEVICE.equals(entityId.getEntityType())) { + DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + RuleChainId ruleChainId; + String queueName; + + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", entityId); + ruleChainId = null; + queueName = ServiceQueue.MAIN; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + String defaultQueueName = deviceProfile.getDefaultQueueName(); + queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + } + return new ImmutablePair<>(queueName, ruleChainId); + } else { + return new ImmutablePair<>(ServiceQueue.MAIN, null); + } + } + + private ListenableFuture processPostTelemetry(TenantId tenantId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { + JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); + metaData.putValue("ts", tsKv.getTs() + ""); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post telemetry [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + return futureToSet; + } + + private ListenableFuture processPostAttributes(TenantId tenantId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post attributes [{}]", msg, t); + futureToSet.setException(t); + } + }); + return futureToSet; + } + + private ListenableFuture processAttributesUpdate(TenantId tenantId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + Set attributes = JsonConverter.convertToAttributes(json); + ListenableFuture> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes)); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List voids) { + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, DataConstants.ATTRIBUTES_UPDATED, entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } + + private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, String entityType) { + SettableFuture futureToSet = SettableFuture.create(); + String scope = attributeDeleteMsg.getScope(); + List attributeNames = attributeDeleteMsg.getAttributeNamesList(); + attributesService.removeAll(tenantId, entityId, scope, attributeNames); + if (EntityType.DEVICE.name().equals(entityType)) { + Set attributeKeys = new HashSet<>(); + for (String attributeName : attributeNames) { + attributeKeys.add(new AttributeKey(scope, attributeName)); + } + tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete( + tenantId, (DeviceId) entityId, attributeKeys), new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attribute delete msg [{}]", attributeDeleteMsg, t); + futureToSet.setException(t); + } + }); + } + return futureToSet; + } + + private EntityId constructEntityId(EntityDataProto entityData) { + EntityType entityType = EntityType.valueOf(entityData.getEntityType()); + switch (entityType) { + case DEVICE: + return new DeviceId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case ASSET: + return new AssetId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case ENTITY_VIEW: + return new EntityViewId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case DASHBOARD: + return new DashboardId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case TENANT: + return new TenantId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case CUSTOMER: + return new CustomerId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case USER: + return new UserId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + default: + log.warn("Unsupported entity type [{}] during construct of entity id. EntityDataProto [{}]", entityData.getEntityType(), entityData); + return null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java index 535a364ff6..5f8b961a46 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java @@ -50,7 +50,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService { @Autowired protected InstallScripts installScripts; - protected abstract void loadSql(Connection conn, String fileName); + protected abstract void loadSql(Connection conn, String fileName, String version); protected void loadFunctions(Path sqlFile, Connection conn) throws Exception { String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index 1c794c95b3..f9ea9afa67 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -57,6 +57,7 @@ public class DatabaseHelper { public static final String DASHBOARD = "dashboard"; public static final String ENTITY_VIEWS = "entity_views"; public static final String ENTITY_VIEW = "entity_view"; + public static final String RULE_CHAIN = "rule_chain"; public static final String ID = "id"; public static final String TITLE = "title"; public static final String TYPE = "type"; diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 6f257a367e..3d21c66e23 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; @@ -274,8 +275,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { thermostatDeviceProfile.setTransportType(DeviceTransportType.DEFAULT); thermostatDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); thermostatDeviceProfile.setDescription("Thermostat device profile"); - thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChains( - demoTenant.getId(), new PageLink(1, 0, "Thermostat")).getData().get(0).getId()); + thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChainsByType( + demoTenant.getId(), RuleChainType.CORE, new PageLink(1, 0, "Thermostat")).getData().get(0).getId()); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); @@ -442,6 +443,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("date"); this.deleteSystemWidgetBundle("entity_admin_widgets"); this.deleteSystemWidgetBundle("navigation_widgets"); + this.deleteSystemWidgetBundle("edge_widgets"); installScripts.loadSystemWidgets(); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index fb7634118b..158cadc1a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -73,6 +73,8 @@ public class InstallScripts { public static final String MODELS_DIR = "models"; public static final String CREDENTIALS_DIR = "credentials"; + public static final String EDGE_MANAGEMENT = "edge_management"; + public static final String JSON_EXT = ".json"; public static final String XML_EXT = ".xml"; @@ -97,14 +99,18 @@ public class InstallScripts { @Autowired private TbResourceService resourceService; - public Path getTenantRuleChainsDir() { + private Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } - public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + private Path getDeviceProfileDefaultRuleChainTemplateFilePath() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); } + private Path getEdgeRuleChainsDir() { + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, EDGE_MANAGEMENT, RULE_CHAINS_DIR); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -128,7 +134,16 @@ public class InstallScripts { public void createDefaultRuleChains(TenantId tenantId) throws IOException { Path tenantChainsDir = getTenantRuleChainsDir(); - try (DirectoryStream dirStream = Files.newDirectoryStream(tenantChainsDir, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { + loadRuleChainsFromPath(tenantId, tenantChainsDir); + } + + public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException { + Path edgeChainsDir = getEdgeRuleChainsDir(); + loadRuleChainsFromPath(tenantId, edgeChainsDir); + } + + private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { + try (DirectoryStream dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { dirStream.forEach( path -> { try { @@ -163,7 +178,6 @@ public class InstallScripts { return ruleChain; } - public void loadSystemWidgets() throws Exception { Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -245,12 +259,18 @@ public class InstallScripts { try { createDefaultRuleChains(tenantId); createDefaultRuleChain(tenantId, "Thermostat"); + loadEdgeDemoRuleChains(tenantId); } catch (Exception e) { log.error("Unable to load dashboard from json", e); throw new RuntimeException("Unable to load dashboard from json", e); } } + private void loadEdgeDemoRuleChains(TenantId tenantId) throws Exception { + Path edgeDemoRuleChainsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, EDGE_MANAGEMENT, RULE_CHAINS_DIR); + loadRuleChainsFromPath(tenantId, edgeDemoRuleChainsDir); + } + public void createOAuth2Templates() throws Exception { Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java index fddec0367d..8e6f4859e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java @@ -94,7 +94,7 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe log.info("PostgreSQL version is valid!"); if (isOldSchema(conn, 2004003)) { log.info("Load upgrade functions ..."); - loadSql(conn, LOAD_FUNCTIONS_SQL); + loadSql(conn, LOAD_FUNCTIONS_SQL, "2.4.3"); log.info("Updating timeseries schema ..."); executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE); if (!partitionType.equals("INDEFINITE")) { @@ -179,9 +179,9 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe } log.info("Load TTL functions ..."); - loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3"); log.info("Load Drop Partitions functions ..."); - loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL); + loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL, "2.4.3"); executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000"); @@ -199,9 +199,9 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe case "3.2.1": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Load TTL functions ..."); - loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3"); log.info("Load Drop Partitions functions ..."); - loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL); + loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL, "2.4.3"); executeQuery(conn, "DROP PROCEDURE IF EXISTS cleanup_timeseries_by_ttl(character varying, bigint, bigint);"); executeQuery(conn, "DROP FUNCTION IF EXISTS delete_asset_records_from_ts_kv(character varying, character varying, bigint);"); @@ -244,8 +244,8 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe } @Override - protected void loadSql(Connection conn, String fileName) { - Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName); + protected void loadSql(Connection conn, String fileName, String version) { + Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName); try { loadFunctions(schemaUpdateFile, conn); log.info("Functions successfully loaded!"); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 6b946bad52..5eba80315c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -465,6 +465,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); installScripts.loadSystemLwm2mResources(); + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + try { + conn.createStatement().execute("ALTER TABLE rule_chain ADD COLUMN type varchar(255) DEFAULT 'CORE'"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception ignored) {} + + log.info("Load Edge TTL functions ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_ttl.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Edge TTL functions successfully loaded!"); + } catch (Exception e) { log.error("Failed updating schema!!!", e); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 112ecc3018..417e3f8f1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -89,7 +89,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr log.info("PostgreSQL version is valid!"); if (isOldSchema(conn, 2004003)) { log.info("Load upgrade functions ..."); - loadSql(conn, LOAD_FUNCTIONS_SQL); + loadSql(conn, LOAD_FUNCTIONS_SQL, "2.4.3"); log.info("Updating timescale schema ..."); executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE); executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE); @@ -165,7 +165,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr } log.info("Load TTL functions ..."); - loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3"); executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000"); log.info("schema timescale updated!"); @@ -178,7 +178,11 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr } break; case "3.1.1": + break; case "3.2.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "3.2.1"); + } break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); @@ -201,8 +205,8 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr } @Override - protected void loadSql(Connection conn, String fileName) { - Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName); + protected void loadSql(Connection conn, String fileName, String version) { + Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName); try { loadFunctions(schemaUpdateFile, conn); log.info("Functions successfully loaded!"); diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index cb987a5e65..ca70c4206e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -37,7 +37,6 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.UUID; @@ -159,6 +158,8 @@ public class CassandraDbHelper { str = Float.valueOf(row.getFloat(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { str = ""+row.getInstant(index).toEpochMilli(); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BOOLEAN) { + str = new Boolean(row.getBoolean(index)).toString(); } else { str = row.getString(index); } @@ -207,6 +208,8 @@ public class CassandraDbHelper { boundStatementBuilder.setFloat(column, Float.valueOf(value)); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { boundStatementBuilder.setInstant(column, Instant.ofEpochMilli(Long.valueOf(value))); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BOOLEAN) { + boundStatementBuilder.setBoolean(column, Boolean.valueOf(value)); } else { boundStatementBuilder.setString(column, value); } 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 f232c0a41e..3f93aef41a 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 @@ -87,6 +87,10 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 3.1.1 to 3.2.0 ..."); tenantsRootRuleChainUpdater.updateEntities(null); break; + case "3.2.2": + log.info("Updating data from version 3.2.2 to 3.3.0 ..."); + tenantsDefaultEdgeRuleChainUpdater.updateEntities(null); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } @@ -113,6 +117,27 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; + private PaginatedUpdater tenantsDefaultEdgeRuleChainUpdater = + new PaginatedUpdater() { + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + try { + RuleChain defaultEdgeRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenant.getId()); + if (defaultEdgeRuleChain == null) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } + } catch (Exception e) { + log.error("Unable to update Tenant", e); + } + } + }; + private PaginatedUpdater tenantsRootRuleChainUpdater = new PaginatedUpdater() { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 6a1dc8f42f..972c99f866 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -20,7 +20,6 @@ import freemarker.template.Configuration; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; @@ -374,7 +373,6 @@ public class DefaultMailService implements MailService { } } - @NotNull private String getValueAsString(long value) { if (value > _1M && value % _1M < _10K) { return value / _1M + "M"; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 1b684863de..2f5610d7ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -31,11 +31,13 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -307,6 +309,21 @@ public class DefaultTbClusterService implements TbClusterService { } } + @Override + public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + log.trace("[{}] Processing edge {} event update ", tenantId, edgeId); + EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); + byte[] msgBytes = encodingService.encode(msg); + TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); + Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); + for (String serviceId : tbCoreServices) { + TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdateMsg(ByteString.copyFrom(msgBytes)).build(); + toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEdgeId().getId(), toCoreMsg), null); + toCoreNfs.incrementAndGet(); + } + } + private void broadcast(ComponentLifecycleMsg msg) { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); @@ -316,7 +333,8 @@ public class DefaultTbClusterService implements TbClusterService { || entityType.equals(EntityType.TENANT_PROFILE) || entityType.equals(EntityType.DEVICE_PROFILE) || entityType.equals(EntityType.API_USAGE_STATE) - || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)) { + || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) + || entityType.equals(EntityType.EDGE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { 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 9d3e2b5a64..23e7f9a849 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 @@ -37,6 +37,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; @@ -56,6 +57,7 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; @@ -101,6 +103,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; @@ -117,7 +120,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray()); if (actorMsg.isPresent()) { @@ -272,6 +280,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreNotification.getEdgeEventUpdateMsg().toByteArray()); + if (actorMsg.isPresent()) { + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); + actorContext.tellWithHighPriority(actorMsg.get()); + } + callback.onSuccess(); } if (statsEnabled) { stats.log(toCoreNotification); @@ -406,6 +421,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService counters = new ArrayList<>(); @@ -66,6 +69,7 @@ public class TbCoreConsumerStats { this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); this.toCoreNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NOTIFICATIONS)); + this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); } private StatsCounter register(StatsCounter counter){ @@ -103,6 +107,11 @@ public class TbCoreConsumerStats { deviceStateCounter.increment(); } + public void log(TransportProtos.EdgeNotificationMsgProto msg) { + totalCounter.increment(); + edgeNotificationsCounter.increment(); + } + public void log(TransportProtos.SubscriptionMgrMsgProto msg) { totalCounter.increment(); subscriptionMsgCounter.increment(); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index 7e8dccb583..c3b038c35a 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -52,7 +52,6 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi private final TbClusterService clusterService; private final TbServiceInfoProvider serviceInfoProvider; - private final ConcurrentMap> toDeviceRpcRequests = new ConcurrentHashMap<>(); private Optional tbCoreRpcService; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java index e73da4805c..b7ce551c0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; import org.thingsboard.rule.engine.api.RpcError; +import java.io.Serializable; import java.util.Optional; import java.util.UUID; @@ -28,7 +29,7 @@ import java.util.UUID; */ @RequiredArgsConstructor @ToString -public class FromDeviceRpcResponse { +public class FromDeviceRpcResponse implements Serializable { @Getter private final UUID id; private final String response; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java new file mode 100644 index 0000000000..9efe9a3b1f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.rpc; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; + +@ToString +@RequiredArgsConstructor +public class FromDeviceRpcResponseActorMsg implements ToDeviceActorNotificationMsg { + + @Getter + private final Integer requestId; + @Getter + private final TenantId tenantId; + @Getter + private final DeviceId deviceId; + + @Getter + private final FromDeviceRpcResponse msg; + + @Override + public MsgType getMsgType() { + return MsgType.DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java index 25a8bd3c01..3b920ac2eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java @@ -28,5 +28,4 @@ public interface TbRuleEngineDeviceRpcService extends RuleEngineRpcService { * @param response the RPC response */ void processRpcResponseFromDevice(FromDeviceRpcResponse response); - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 748cad8247..1627002fb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -33,12 +33,14 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -54,6 +56,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.rule.RuleChainService; @@ -83,6 +86,7 @@ public class AccessValidator { public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; + public static final String EDGE_WITH_REQUESTED_ID_NOT_FOUND = "Edge with requested id wasn't found!"; public static final String ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND = "Entity-view with requested id wasn't found!"; @Autowired @@ -112,6 +116,9 @@ public class AccessValidator { @Autowired protected EntityViewService entityViewService; + @Autowired(required = false) + protected EdgeService edgeService; + @Autowired protected AccessControlService accessControlService; @@ -204,6 +211,9 @@ public class AccessValidator { case ENTITY_VIEW: validateEntityView(currentUser, operation, entityId, callback); return; + case EDGE: + validateEdge(currentUser, operation, entityId, callback); + return; case API_USAGE_STATE: validateApiUsageState(currentUser, operation, entityId, callback); return; @@ -423,6 +433,26 @@ public class AccessValidator { } } + private void validateEdge(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture edgeFuture = edgeService.findEdgeByIdAsync(currentUser.getTenantId(), new EdgeId(entityId.getId())); + Futures.addCallback(edgeFuture, getCallback(callback, edge -> { + if (edge == null) { + return ValidationResult.entityNotFound(EDGE_WITH_REQUESTED_ID_NOT_FOUND); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.EDGE, operation, entityId, edge); + } catch (ThingsboardException e) { + return ValidationResult.accessDenied(e.getMessage()); + } + return ValidationResult.ok(edge); + } + }), executor); + } + } + private FutureCallback getCallback(FutureCallback callback, Function> transformer) { return new FutureCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index f56d5dc17e..fc1705d688 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -40,6 +40,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); + put(Resource.EDGE, customerEntityPermissionChecker); } private static final PermissionChecker customerEntityPermissionChecker = diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java index ead0c52fb7..074943f1a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.permission; public enum Operation { ALL, CREATE, READ, WRITE, DELETE, ASSIGN_TO_CUSTOMER, UNASSIGN_FROM_CUSTOMER, RPC_CALL, - READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES, ASSIGN_TO_TENANT + READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES, + ASSIGN_TO_TENANT, ASSIGN_TO_EDGE, UNASSIGN_FROM_EDGE } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index ba53ceea98..54d4667903 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -37,7 +37,8 @@ public enum Resource { TENANT_PROFILE(EntityType.TENANT_PROFILE), DEVICE_PROFILE(EntityType.DEVICE_PROFILE), API_USAGE_STATE(EntityType.API_USAGE_STATE), - TB_RESOURCE(EntityType.TB_RESOURCE); + TB_RESOURCE(EntityType.TB_RESOURCE), + EDGE(EntityType.EDGE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index c36a70821f..a5ccae2bd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -42,6 +42,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); put(Resource.TB_RESOURCE, tbResourcePermissionChecker); + put(Resource.EDGE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java new file mode 100644 index 0000000000..0c21719451 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ttl.edge; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.service.ttl.AbstractCleanUpService; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +@PsqlDao +@Slf4j +@Service +public class EdgeEventsCleanUpService extends AbstractCleanUpService { + + @Value("${sql.ttl.edge_events.edge_events_ttl}") + private long ttl; + + @Value("${sql.ttl.edge_events.enabled}") + private boolean ttlTaskExecutionEnabled; + + @Scheduled(initialDelayString = "${sql.ttl.edge_events.execution_interval_ms}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") + public void cleanUp() { + if (ttlTaskExecutionEnabled) { + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + doCleanUp(conn); + } catch (SQLException e) { + log.error("SQLException occurred during TTL task execution ", e); + } + } + } + + @Override + protected void doCleanUp(Connection connection) throws SQLException { + long totalEdgeEventsRemoved = executeQuery(connection, "call cleanup_edge_events_by_ttl(" + ttl + ", 0);"); + log.info("Total edge events removed by TTL: [{}]", totalEdgeEventsRemoved); + } +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 21c0b9f970..519d0b5c94 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -267,6 +267,10 @@ sql: execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day events_ttl: "${SQL_TTL_EVENTS_EVENTS_TTL:0}" # Number of seconds debug_events_ttl: "${SQL_TTL_EVENTS_DEBUG_EVENTS_TTL:604800}" # Number of seconds. The current value corresponds to one week + edge_events: + enabled: "${SQL_TTL_EDGE_EVENTS_ENABLED:true}" + execution_interval_ms: "${SQL_TTL_EDGE_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day + edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month # Actor system parameters actors: @@ -365,6 +369,9 @@ caffeine: tokensOutdatageTime: timeToLiveInMinutes: 20000 maxSize: 10000 + edges: + timeToLiveInMinutes: 1440 + maxSize: 0 redis: # standalone or cluster @@ -480,6 +487,7 @@ audit-log: "alarm": "${AUDIT_LOG_MASK_ALARM:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" + "edge": "${AUDIT_LOG_MASK_EDGE:W}" sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -586,6 +594,28 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" # Local LwM2M transport parameters lwm2m: # Enable/disable lvm2m transport protocol. @@ -596,8 +626,7 @@ transport: timeout: "${LWM2M_TIMEOUT:120000}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" - request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" - request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" @@ -606,8 +635,7 @@ transport: # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" -# key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" key_store_path_file: "${KEY_STORE_PATH_FILE:}" key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" @@ -626,24 +654,26 @@ transport: # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" bootstrap: - enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" - id: "${LWM2M_SERVER_ID:111}" + enable: "${LWM2M_ENABLED_BS:true}" + id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" secure: bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: - alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BS:bootstrap}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" snmp: @@ -654,6 +684,26 @@ transport: # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" +# Edges parameters +edges: + enabled: "${EDGES_ENABLED:false}" + rpc: + port: "${EDGES_RPC_PORT:7070}" + client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:300}" + ssl: + # Enable/disable SSL support + enabled: "${EDGES_RPC_SSL_ENABLED:false}" + cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}" + private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}" + storage: + max_read_records_count: "${EDGES_STORAGE_MAX_READ_RECORDS_COUNT:50}" + no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" + sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:1000}" + scheduler_pool_size: "${EDGES_SCHEDULER_POOL_SIZE:4}" + edge_events_ttl: "${EDGES_EDGE_EVENTS_TTL:0}" + state: + persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}" + swagger: api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}" security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}" 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 e25592e35e..2329e8086c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -23,6 +23,7 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matcher; import org.junit.After; @@ -57,6 +58,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttTopics; @@ -465,6 +467,10 @@ public abstract class AbstractWebTest { return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); } + protected R doPostWithTypedResponse(String urlTemplate, T content, TypeReference responseType, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseType); + } + protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } @@ -558,4 +564,18 @@ public abstract class AbstractWebTest { return jsonPath("$.message", matcher); } + protected Edge constructEdge(String name, String type) { + return constructEdge(tenantId, name, type); + } + protected Edge constructEdge(TenantId tenantId, String name, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName(name); + edge.setType(type); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 5ec0ed4fe0..244eb0e05d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -672,4 +673,30 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(0, pageData.getData().size()); } + @Test + public void testAssignAssetToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Asset asset = new Asset(); + asset.setName("My asset"); + asset.setType("default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java index 635505c1c5..977583b4c9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import java.util.List; @@ -81,14 +82,14 @@ public abstract class BaseComponentDescriptorControllerTest extends AbstractCont @Test public void testGetByType() throws Exception { List descriptors = readResponse( - doGet("/api/components/" + ComponentType.FILTER).andExpect(status().isOk()), new TypeReference>() { + doGet("/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", ComponentType.FILTER, RuleChainType.CORE).andExpect(status().isOk()), new TypeReference>() { }); Assert.assertNotNull(descriptors); Assert.assertTrue(descriptors.size() >= AMOUNT_OF_DEFAULT_FILTER_NODES); for (ComponentType type : ComponentType.values()) { - doGet("/api/components/" + type).andExpect(status().isOk()); + doGet("/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", type, RuleChainType.CORE).andExpect(status().isOk()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index 6e19ff8b13..60b4128878 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -15,27 +15,30 @@ */ package org.thingsboard.server.controller; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; -import org.thingsboard.server.common.data.*; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.security.Authority; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; -import com.fasterxml.jackson.core.type.TypeReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseDashboardControllerTest extends AbstractControllerTest { @@ -348,4 +351,30 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(dashboards, loadedDashboards); } + @Test + public void testAssignDashboardToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/dashboards?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/dashboards?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index e6cb4667ba..fa5a6b76eb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -24,10 +24,10 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; @@ -463,7 +463,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title1); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", + pageData = doGetTypedWithPageLink("/api/tenant/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -473,7 +473,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title2); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", + pageData = doGetTypedWithPageLink("/api/tenant/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -669,7 +669,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title1); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -679,7 +679,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title2); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -827,4 +827,31 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) .andExpect(status().isOk()); } + + @Test + public void testAssignDeviceToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java new file mode 100644 index 0000000000..ba2d3ce61e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -0,0 +1,706 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +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.security.Authority; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.edge.imitator.EdgeImitator; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +public abstract class BaseEdgeControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + + private Tenant savedTenant; + private TenantId tenantId; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getEdgeLicenseKey())); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getCloudEndpoint())); + + savedEdge.setName("My new edge"); + doPost("/api/edge", savedEdge, Edge.class); + + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); + } + + @Test + public void testFindEdgeById() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertNotNull(foundEdge); + Assert.assertEquals(savedEdge, foundEdge); + } + + @Test + public void testFindEdgeTypesByTenantId() throws Exception { + List edges = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Edge edge = constructEdge("My edge B" + i, "typeB"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + for (int i = 0; i < 7; i++) { + Edge edge = constructEdge("My edge C" + i, "typeC"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + for (int i = 0; i < 9; i++) { + Edge edge = constructEdge("My edge A" + i, "typeA"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + List edgeTypes = doGetTyped("/api/edge/types", + new TypeReference>() { + }); + + Assert.assertNotNull(edgeTypes); + Assert.assertEquals(3, edgeTypes.size()); + Assert.assertEquals("typeA", edgeTypes.get(0).getType()); + Assert.assertEquals("typeB", edgeTypes.get(1).getType()); + Assert.assertEquals("typeC", edgeTypes.get(2).getType()); + } + + @Test + public void testDeleteEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveEdgeWithEmptyType() throws Exception { + Edge edge = constructEdge("My edge", null); + doPost("/api/edge", edge) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Edge type should be specified"))); + } + + @Test + public void testSaveEdgeWithEmptyName() throws Exception { + Edge edge = constructEdge(null, "default"); + doPost("/api/edge", edge) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Edge name should be specified"))); + } + + @Test + public void testAssignUnassignEdgeToCustomer() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Customer customer = new Customer(); + customer.setTitle("My customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + Edge assignedEdge = doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(savedCustomer.getId(), assignedEdge.getCustomerId()); + + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(savedCustomer.getId(), foundEdge.getCustomerId()); + + Edge unassignedEdge = + doDelete("/api/customer/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(ModelConstants.NULL_UUID, unassignedEdge.getCustomerId().getId()); + + foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(ModelConstants.NULL_UUID, foundEdge.getCustomerId().getId()); + } + + @Test + public void testAssignEdgeToNonExistentCustomer() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doPost("/api/customer/" + Uuids.timeBased().toString() + + "/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testAssignEdgeToCustomerFromDifferentTenant() throws Exception { + loginSysAdmin(); + + Tenant tenant2 = new Tenant(); + tenant2.setTitle("Different tenant"); + Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class); + Assert.assertNotNull(savedTenant2); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(savedTenant2.getId()); + tenantAdmin2.setEmail("tenant3@thingsboard.org"); + tenantAdmin2.setFirstName("Joe"); + tenantAdmin2.setLastName("Downs"); + + tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1"); + + Customer customer = new Customer(); + customer.setTitle("Different customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + login(tenantAdmin.getEmail(), "testPassword1"); + + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isForbidden()); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant2.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testFindTenantEdges() throws Exception { + List edges = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + Edge edge = constructEdge("Edge" + i, "default"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + } + + @Test + public void testFindTenantEdgesByName() throws Exception { + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle1.add(doPost("/api/edge", edge, Edge.class)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 75; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle2.add(doPost("/api/edge", edge, Edge.class)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindTenantEdgesByType() throws Exception { + String title1 = "Edge title 1"; + String type1 = "typeA"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edgesType1.add(doPost("/api/edge", edge, Edge.class)); + } + String title2 = "Edge title 2"; + String type2 = "typeB"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 75; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edgesType2.add(doPost("/api/edge", edge, Edge.class)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4); + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4); + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4); + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindCustomerEdges() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + Edge edge = constructEdge("Edge" + i, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edges.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + } + + @Test + public void testFindCustomerEdgesByName() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 125; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edgesTitle1.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edgesTitle2.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15,0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4,0, title2); + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindCustomerEdgesByType() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + String type1 = "typeC"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 125; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edge = doPost("/api/edge", edge, Edge.class); + edgesType1.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + String title2 = "Edge title 2"; + String type2 = "typeD"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edge = doPost("/api/edge", edge, Edge.class); + edgesType2.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testSyncEdge() throws Exception { + Edge edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + + Device device = new Device(); + device.setName("Edge Device 1"); + device.setType("test"); + Device savedDevice = doPost("/api/device", device, Device.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + Asset asset = new Asset(); + asset.setName("Edge Asset 1"); + asset.setType("test"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + EdgeImitator edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); + edgeImitator.expectMessageAmount(7); + edgeImitator.connect(); + edgeImitator.waitForMessages(); + + Assert.assertEquals(7, edgeImitator.getDownlinkMsgs().size()); + Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(AssetUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(UserUpdateMsg.class).isPresent()); + + edgeImitator.getDownlinkMsgs().clear(); + + edgeImitator.expectMessageAmount(4); + doPost("/api/edge/sync/" + edge.getId()); + edgeImitator.waitForMessages(); + + Assert.assertEquals(4, edgeImitator.getDownlinkMsgs().size()); + Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(AssetUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(UserUpdateMsg.class).isPresent()); + + edgeImitator.allowIgnoredTypes(); + edgeImitator.disconnect(); + + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java new file mode 100644 index 0000000000..5edde458ce --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java @@ -0,0 +1,125 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public class BaseEdgeEventControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + private TenantId tenantId; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testGetEdgeEvents() throws Exception { + Thread.sleep(500); + Edge edge = constructEdge("TestEdge", "default"); + edge = doPost("/api/edge", edge, Edge.class); + + Device device = constructDevice("TestDevice", "default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/edge/" + edge.getId().toString() + "/device/" + savedDevice.getId().toString(), Device.class); + Thread.sleep(500); + + Asset asset = constructAsset("TestAsset", "default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + + doPost("/api/edge/" + edge.getId().toString() + "/asset/" + savedAsset.getId().toString(), Asset.class); + Thread.sleep(500); + + EntityRelation relation = new EntityRelation(savedAsset.getId(), savedDevice.getId(), EntityRelation.CONTAINS_TYPE); + + doPost("/api/relation", relation); + Thread.sleep(500); + + List edgeEvents = doGetTypedWithTimePageLink("/api/edge/" + edge.getId().toString() + "/events?", + new TypeReference>() { + }, new TimePageLink(4)).getData(); + + Assert.assertFalse(edgeEvents.isEmpty()); + Assert.assertEquals(4, edgeEvents.size()); + Assert.assertEquals(EdgeEventType.RULE_CHAIN, edgeEvents.get(0).getType()); + Assert.assertEquals(EdgeEventType.DEVICE, edgeEvents.get(1).getType()); + Assert.assertEquals(EdgeEventType.ASSET, edgeEvents.get(2).getType()); + Assert.assertEquals(EdgeEventType.RELATION, edgeEvents.get(3).getType()); + } + + private Device constructDevice(String name, String type) { + Device device = new Device(); + device.setName(name); + device.setType(type); + return device; + } + + private Asset constructAsset(String name, String type) { + Asset asset = new Asset(); + asset.setName(name); + asset.setType(type); + return asset; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index 5d234d2691..b2fe4a563b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -16,16 +16,10 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.ssl.SSLContextBuilder; -import org.apache.http.ssl.SSLContexts; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.boot.web.server.LocalServerPort; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -33,7 +27,6 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -49,16 +42,10 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.stream.Collectors; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index c6d0073d83..e651caa2ff 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -27,7 +27,13 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; @@ -49,7 +55,6 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -568,4 +573,31 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes return loadedItems; } + + @Test + public void testAssignEntityViewToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + EntityView savedEntityView = getNewSavedEntityView("My entityView"); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + testDevice.getId().getId().toString(), Device.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/entityViews?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/entityViews?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java index 31c77f2830..8850b70f86 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java @@ -79,7 +79,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - TbResource savedResource = doPost("/api/resource", resource, TbResource.class); + TbResource savedResource = save(resource); Assert.assertNotNull(savedResource); Assert.assertNotNull(savedResource.getId()); @@ -92,7 +92,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes savedResource.setTitle("My new resource"); - doPost("/api/resource", savedResource, TbResource.class); + save(savedResource); TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); @@ -106,10 +106,10 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - TbResource savedResource = doPost("/api/resource", resource, TbResource.class); + TbResource savedResource = save(resource); loginDifferentTenant(); - doPost("/api/resource", savedResource, TbResource.class, status().isForbidden()); + doPostWithTypedResponse("/api/resource", savedResource, new TypeReference<>(){}, status().isForbidden()); deleteDifferentTenant(); } @@ -121,7 +121,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - TbResource savedResource = doPost("/api/resource", resource, TbResource.class); + TbResource savedResource = save(resource); TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); Assert.assertNotNull(foundResource); @@ -136,7 +136,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - TbResource savedResource = doPost("/api/resource", resource, TbResource.class); + TbResource savedResource = save(resource); doDelete("/api/resource/" + savedResource.getId().getId().toString()) .andExpect(status().isOk()); @@ -154,7 +154,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData("Test Data"); - resources.add(new TbResourceInfo(doPost("/api/resource", resource, TbResource.class))); + resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); PageLink pageLink = new PageLink(24); @@ -186,7 +186,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData("Test Data"); - resources.add(new TbResourceInfo(doPost("/api/resource", resource, TbResource.class))); + resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); PageLink pageLink = new PageLink(24); @@ -236,7 +236,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData("Test Data"); - expectedResources.add(new TbResourceInfo(doPost("/api/resource", resource, TbResource.class))); + expectedResources.add(new TbResourceInfo(save(resource))); } loginSysAdmin(); @@ -247,7 +247,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData("Test Data"); - TbResourceInfo savedResource = new TbResourceInfo(doPost("/api/resource", resource, TbResource.class)); + TbResourceInfo savedResource = new TbResourceInfo(save(resource)); systemResources.add(savedResource); if (i >= 73) { expectedResources.add(savedResource); @@ -281,4 +281,8 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes .andExpect(status().isOk()); } } + + private TbResource save(TbResource tbResource) throws Exception { + return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>(){}); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index 0f969a848f..653f6978f6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -28,6 +28,7 @@ import java.util.Arrays; @ClasspathSuite.ClassnameFilters({ // "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", // "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", +// "org.thingsboard.server.controller.sql.TbResourceControllerSqlTest", "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java new file mode 100644 index 0000000000..0d1fa48f7e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseEdgeControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeControllerSqlTest extends BaseEdgeControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java new file mode 100644 index 0000000000..b38e00982f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseEdgeEventControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeEventControllerSqlTest extends BaseEdgeEventControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java new file mode 100644 index 0000000000..6622a744b5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -0,0 +1,1465 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonObject; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +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.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.edge.imitator.EdgeImitator; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RpcResponseMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.queue.TbClusterService; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +abstract public class BaseEdgeTest extends AbstractControllerTest { + + private Tenant savedTenant; + private TenantId tenantId; + private User tenantAdmin; + + private EdgeImitator edgeImitator; + private Edge edge; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private TbClusterService clusterService; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + installation(); + + edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + // should be less, but events from SyncEdgeService stack with events from controller. will be fixed in next releases + edgeImitator.expectMessageAmount(7); + edgeImitator.connect(); + } + + @After + public void afterTest() throws Exception { + edgeImitator.disconnect(); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + + @Test + public void test() throws Exception { + testReceivedInitialData(); + testDevices(); + testAssets(); + testRuleChains(); + testDashboards(); + testRelations(); + testAlarms(); + testEntityView(); + testCustomer(); + testWidgetsBundleAndWidgetType(); + testTimeseries(); + testAttributes(); + testSendMessagesToCloud(); + testRpcCall(); + } + + private Device findDeviceByName(String deviceName) throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Optional foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny(); + Assert.assertTrue(foundDevice.isPresent()); + Device device = foundDevice.get(); + Assert.assertEquals(deviceName, device.getName()); + return device; + } + + private Asset findAssetByName(String assetName) throws Exception { + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/assets?", + new TypeReference>() { + }, new PageLink(100)).getData(); + + Assert.assertEquals(1, edgeAssets.size()); + Asset asset = edgeAssets.get(0); + Assert.assertEquals(assetName, asset.getName()); + return asset; + } + + private Device saveDevice(String deviceName) throws Exception { + Device device = new Device(); + device.setName(deviceName); + device.setType("test"); + return doPost("/api/device", device, Device.class); + } + + private Asset saveAsset(String assetName) throws Exception { + Asset asset = new Asset(); + asset.setName(assetName); + asset.setType("test"); + return doPost("/api/asset", asset, Asset.class); + } + + private void testRpcCall() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + ObjectNode body = mapper.createObjectNode(); + body.put("requestId", new Random().nextInt()); + body.put("requestUUID", Uuids.timeBased().toString()); + body.put("oneway", false); + body.put("expirationTime", System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10)); + body.put("method", "test_method"); + body.put("params", "{\"param1\":\"value1\"}"); + + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceRpcCallMsg); + DeviceRpcCallMsg latestDeviceRpcCallMsg = (DeviceRpcCallMsg) latestMessage; + Assert.assertEquals("test_method", latestDeviceRpcCallMsg.getRequestMsg().getMethod()); + } + + private void testReceivedInitialData() throws Exception { + log.info("Checking received data"); + edgeImitator.waitForMessages(); + + EdgeConfiguration configuration = edgeImitator.getConfiguration(); + Assert.assertNotNull(configuration); + + testAutoGeneratedCodeByProtobuf(configuration); + + UserId userId = edgeImitator.getUserId(); + Assert.assertNotNull(userId); + + Optional optionalMsg1 = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(optionalMsg1.isPresent()); + DeviceUpdateMsg deviceUpdateMsg = optionalMsg1.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB()); + Device device = doGet("/api/device/" + deviceUUID.toString(), Device.class); + Assert.assertNotNull(device); + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeDevices.contains(device)); + + Optional optionalMsg2 = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(optionalMsg2.isPresent()); + AssetUpdateMsg assetUpdateMsg = optionalMsg2.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + UUID assetUUID = new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB()); + Asset asset = doGet("/api/asset/" + assetUUID.toString(), Asset.class); + Assert.assertNotNull(asset); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeAssets.contains(asset)); + + testAutoGeneratedCodeByProtobuf(assetUpdateMsg); + + Optional optionalMsg3 = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); + Assert.assertTrue(optionalMsg3.isPresent()); + RuleChainUpdateMsg ruleChainUpdateMsg = optionalMsg3.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + UUID ruleChainUUID = new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB()); + RuleChain ruleChain = doGet("/api/ruleChain/" + ruleChainUUID.toString(), RuleChain.class); + Assert.assertNotNull(ruleChain); + List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/ruleChains?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeRuleChains.contains(ruleChain)); + + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsg); + + log.info("Received data checked"); + } + + private void testDevices() throws Exception { + log.info("Testing devices"); + + Device savedDevice = saveDevice("Edge Device 2"); + + edgeImitator.expectMessageAmount(1); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getName(), savedDevice.getName()); + Assert.assertEquals(deviceUpdateMsg.getType(), savedDevice.getType()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + + log.info("Devices tested successfully"); + } + + + private void testAssets() throws Exception { + log.info("Testing assets"); + Asset savedAsset = saveAsset("Edge Asset 2"); + + edgeImitator.expectMessageAmount(1); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getName(), savedAsset.getName()); + Assert.assertEquals(assetUpdateMsg.getType(), savedAsset.getType()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + + log.info("Assets tested successfully"); + } + + private void testRuleChains() throws Exception { + log.info("Testing RuleChains"); + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("Edge Test Rule Chain"); + ruleChain.setType(RuleChainType.EDGE); + RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + + createRuleChainMetadata(savedRuleChain); + + // Wait before rule chain metadata saved to database before rule chain is assigned to edge + Thread.sleep(1000); + + edgeImitator.expectMessageAmount(1); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); + RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); + + testRuleChainMetadataRequestMsg(savedRuleChain.getId()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); + ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/ruleChain/" + savedRuleChain.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); + ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + + log.info("RuleChains tested successfully"); + } + + private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception { + RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder() + .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() + .addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; + RuleChainId receivedRuleChainId = + new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); + Assert.assertEquals(ruleChainId, receivedRuleChainId); + } + + private void createRuleChainMetadata(RuleChain ruleChain) throws Exception { + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + + ObjectMapper mapper = new ObjectMapper(); + + RuleNode ruleNode1 = new RuleNode(); + ruleNode1.setName("name1"); + ruleNode1.setType("type1"); + ruleNode1.setConfiguration(mapper.readTree("\"key1\": \"val1\"")); + + RuleNode ruleNode2 = new RuleNode(); + ruleNode2.setName("name2"); + ruleNode2.setType("type2"); + ruleNode2.setConfiguration(mapper.readTree("\"key2\": \"val2\"")); + + RuleNode ruleNode3 = new RuleNode(); + ruleNode3.setName("name3"); + ruleNode3.setType("type3"); + ruleNode3.setConfiguration(mapper.readTree("\"key3\": \"val3\"")); + + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode1); + ruleNodes.add(ruleNode2); + ruleNodes.add(ruleNode3); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData.addConnectionInfo(0, 1, "success"); + ruleChainMetaData.addConnectionInfo(0, 2, "fail"); + ruleChainMetaData.addConnectionInfo(1, 2, "success"); + + ruleChainMetaData.addRuleChainConnectionInfo(2, edge.getRootRuleChainId(), "success", mapper.createObjectNode()); + + doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + } + + private void testDashboards() throws Exception { + log.info("Testing Dashboards"); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("Edge Test Dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + edgeImitator.expectMessageAmount(1); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getTitle(), savedDashboard.getName()); + + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); + + edgeImitator.expectMessageAmount(1); + savedDashboard.setTitle("Updated Edge Test Dashboard"); + doPost("/api/dashboard", savedDashboard, Dashboard.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getTitle(), savedDashboard.getName()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/dashboard/" + savedDashboard.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + + log.info("Dashboards tested successfully"); + } + + private void testRelations() throws Exception { + log.info("Testing Relations"); + + Device device = findDeviceByName("Edge Device 1"); + Asset asset = findAssetByName("Edge Asset 1"); + + EntityRelation relation = new EntityRelation(); + relation.setType("test"); + relation.setFrom(device.getId()); + relation.setTo(asset.getId()); + relation.setTypeGroup(RelationTypeGroup.COMMON); + + edgeImitator.expectMessageAmount(1); + doPost("/api/relation", relation); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/relation?" + + "fromId=" + relation.getFrom().getId().toString() + + "&fromType=" + relation.getFrom().getEntityType().name() + + "&relationType=" + relation.getType() + + "&relationTypeGroup=" + relation.getTypeGroup().name() + + "&toId=" + relation.getTo().getId().toString() + + "&toType=" + relation.getTo().getEntityType().name()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); + + log.info("Relations tested successfully"); + } + + private void testAlarms() throws Exception { + log.info("Testing Alarms"); + 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); + Alarm savedAlarm = doPost("/api/alarm", alarm, Alarm.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), savedAlarm.getStatus().name()); + Assert.assertEquals(alarmUpdateMsg.getSeverity(), savedAlarm.getSeverity().name()); + + edgeImitator.expectMessageAmount(1); + doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/ack"); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ALARM_ACK_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.ACTIVE_ACK.name()); + + edgeImitator.expectMessageAmount(1); + doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/clear"); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.CLEARED_ACK.name()); + + doDelete("/api/alarm/" + savedAlarm.getId().getId().toString()) + .andExpect(status().isOk()); + log.info("Alarms tested successfully"); + } + + private void testEntityView() throws Exception { + log.info("Testing EntityView"); + Device device = findDeviceByName("Edge Device 1"); + + EntityView entityView = new EntityView(); + entityView.setName("Edge EntityView 1"); + entityView.setType("test"); + entityView.setEntityId(device.getId()); + EntityView savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + + edgeImitator.expectMessageAmount(1); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); + EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(entityViewUpdateMsg.getType(), savedEntityView.getType()); + Assert.assertEquals(entityViewUpdateMsg.getName(), savedEntityView.getName()); + Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityType().name(), device.getId().getEntityType().name()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); + entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/entityView/" + savedEntityView.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); + entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + + log.info("EntityView tested successfully"); + } + + private void testCustomer() throws Exception { + log.info("Testing Customer"); + + Customer customer = new Customer(); + customer.setTitle("Edge Customer 1"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + edgeImitator.expectMessageAmount(1); + doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); + CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getTitle(), savedCustomer.getTitle()); + + testAutoGeneratedCodeByProtobuf(customerUpdateMsg); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/customer/edge/" + edge.getId().getId().toString(), Edge.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); + customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); + customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + + log.info("Customer tested successfully"); + } + + private void testWidgetsBundleAndWidgetType() throws Exception { + log.info("Testing WidgetsBundle and WidgetType"); + + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("Test Widget Bundle"); + + edgeImitator.expectMessageAmount(1); + WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdMSB(), savedWidgetsBundle.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdLSB(), savedWidgetsBundle.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getAlias(), savedWidgetsBundle.getAlias()); + Assert.assertEquals(widgetsBundleUpdateMsg.getTitle(), savedWidgetsBundle.getTitle()); + + testAutoGeneratedCodeByProtobuf(widgetsBundleUpdateMsg); + + WidgetType widgetType = new WidgetType(); + widgetType.setName("Test Widget Type"); + widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); + ObjectNode descriptor = mapper.createObjectNode(); + descriptor.put("key", "value"); + widgetType.setDescriptor(descriptor); + + edgeImitator.expectMessageAmount(1); + WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); + WidgetTypeUpdateMsg widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdMSB(), savedWidgetType.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdLSB(), savedWidgetType.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getAlias(), savedWidgetType.getAlias()); + Assert.assertEquals(widgetTypeUpdateMsg.getName(), savedWidgetType.getName()); + Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/widgetType/" + savedWidgetType.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); + widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdMSB(), savedWidgetType.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdLSB(), savedWidgetType.getUuidId().getLeastSignificantBits()); + + edgeImitator.expectMessageAmount(1); + doDelete("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString()) + .andExpect(status().isOk()); + edgeImitator.waitForMessages(); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); + widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdMSB(), savedWidgetsBundle.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdLSB(), savedWidgetsBundle.getUuidId().getLeastSignificantBits()); + + log.info("WidgetsBundle and WidgetType tested successfully"); + } + + private void testTimeseries() throws Exception { + log.info("Testing timeseries"); + Device device = findDeviceByName("Edge Device 1"); + + String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = mapper.readTree(timeseriesData); + EdgeEvent edgeEvent1 = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent1); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(latestEntityDataMsg.getEntityIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(latestEntityDataMsg.getEntityIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(latestEntityDataMsg.getEntityType(), device.getId().getEntityType().name()); + Assert.assertTrue(latestEntityDataMsg.hasPostTelemetryMsg()); + + TransportProtos.PostTelemetryMsg postTelemetryMsg = latestEntityDataMsg.getPostTelemetryMsg(); + Assert.assertEquals(1, postTelemetryMsg.getTsKvListCount()); + TransportProtos.TsKvListProto tsKvListProto = postTelemetryMsg.getTsKvList(0); + Assert.assertEquals(timeseriesEntityData.get("ts").asLong(), tsKvListProto.getTs()); + Assert.assertEquals(1, tsKvListProto.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = tsKvListProto.getKv(0); + Assert.assertEquals("temperature", keyValueProto.getKey()); + Assert.assertEquals(25, keyValueProto.getLongV()); + log.info("Timeseries tested successfully"); + } + + private void testAttributes() throws Exception { + log.info("Testing attributes"); + Device device = findDeviceByName("Edge Device 1"); + + testAttributesUpdatedMsg(device); + testPostAttributesMsg(device); + testAttributesDeleteMsg(device); + + log.info("Attributes tested successfully"); + } + + private void testAttributesDeleteMsg(Device device) throws JsonProcessingException, InterruptedException { + String deleteAttributesData = "{\"scope\":\"SERVER_SCOPE\",\"keys\":[\"key1\",\"key2\"]}"; + JsonNode deleteAttributesEntityData = mapper.readTree(deleteAttributesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.ATTRIBUTES_DELETED, device.getId().getId(), EdgeEventType.DEVICE, deleteAttributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + + Assert.assertTrue(latestEntityDataMsg.hasAttributeDeleteMsg()); + + AttributeDeleteMsg attributeDeleteMsg = latestEntityDataMsg.getAttributeDeleteMsg(); + Assert.assertEquals(attributeDeleteMsg.getScope(), deleteAttributesEntityData.get("scope").asText()); + + Assert.assertEquals(2, attributeDeleteMsg.getAttributeNamesCount()); + Assert.assertEquals("key1", attributeDeleteMsg.getAttributeNames(0)); + Assert.assertEquals("key2", attributeDeleteMsg.getAttributeNames(1)); + } + + private void testPostAttributesMsg(Device device) throws JsonProcessingException, InterruptedException { + String postAttributesData = "{\"scope\":\"SERVER_SCOPE\",\"kv\":{\"key2\":\"value2\"}}"; + JsonNode postAttributesEntityData = mapper.readTree(postAttributesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.POST_ATTRIBUTES, device.getId().getId(), EdgeEventType.DEVICE, postAttributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals("SERVER_SCOPE", latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasPostAttributesMsg()); + + TransportProtos.PostAttributeMsg postAttributesMsg = latestEntityDataMsg.getPostAttributesMsg(); + Assert.assertEquals(1, postAttributesMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = postAttributesMsg.getKv(0); + Assert.assertEquals("key2", keyValueProto.getKey()); + Assert.assertEquals("value2", keyValueProto.getStringV()); + } + + private void testAttributesUpdatedMsg(Device device) throws JsonProcessingException, InterruptedException { + String attributesData = "{\"scope\":\"SERVER_SCOPE\",\"kv\":{\"key1\":\"value1\"}}"; + JsonNode attributesEntityData = mapper.readTree(attributesData); + EdgeEvent edgeEvent1 = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.ATTRIBUTES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, attributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent1); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals("SERVER_SCOPE", latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasAttributesUpdatedMsg()); + + TransportProtos.PostAttributeMsg attributesUpdatedMsg = latestEntityDataMsg.getAttributesUpdatedMsg(); + Assert.assertEquals(1, attributesUpdatedMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = attributesUpdatedMsg.getKv(0); + Assert.assertEquals("key1", keyValueProto.getKey()); + Assert.assertEquals("value1", keyValueProto.getStringV()); + } + + private void testSendMessagesToCloud() throws Exception { + log.info("Sending messages to cloud"); + sendDevice(); + sendDeviceWithNameThatAlreadyExistsOnCloud(); + sendRelationRequest(); + sendAlarm(); + sendTelemetry(); + sendRelation(); + sendDeleteDeviceOnEdge(); + sendRuleChainMetadataRequest(); + sendUserCredentialsRequest(); + sendDeviceCredentialsRequest(); + sendDeviceRpcResponse(); + sendDeviceCredentialsUpdate(); + sendAttributesRequest(); + log.info("Messages were sent successfully"); + } + + private void sendDevice() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName("Edge Device 2"); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg latestDeviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals("Edge Device 2", latestDeviceUpdateMsg.getName()); + + UUID newDeviceId = new UUID(latestDeviceUpdateMsg.getIdMSB(), latestDeviceUpdateMsg.getIdLSB()); + + Device device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertEquals("Edge Device 2", device.getName()); + } + + private void sendDeviceWithNameThatAlreadyExistsOnCloud() throws Exception { + String deviceOnCloudName = RandomStringUtils.randomAlphanumeric(15); + Device deviceOnCloud = saveDevice(deviceOnCloudName); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName(deviceOnCloudName); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg latestDeviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertNotEquals(deviceOnCloudName, latestDeviceUpdateMsg.getName()); + Assert.assertEquals(deviceOnCloudName, latestDeviceUpdateMsg.getConflictName()); + + UUID newDeviceId = new UUID(latestDeviceUpdateMsg.getIdMSB(), latestDeviceUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(deviceOnCloud.getId().getId(), newDeviceId); + + Device device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertNotEquals(deviceOnCloudName, device.getName()); + } + + private void sendRelationRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + Asset asset = findAssetByName("Edge Asset 1"); + + EntityRelation relation = new EntityRelation(); + relation.setType("test"); + relation.setFrom(device.getId()); + relation.setTo(asset.getId()); + relation.setTypeGroup(RelationTypeGroup.COMMON); + + edgeImitator.expectMessageAmount(1); + doPost("/api/relation", relation); + edgeImitator.waitForMessages(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RelationRequestMsg.Builder relationRequestMsgBuilder = RelationRequestMsg.newBuilder(); + relationRequestMsgBuilder.setEntityIdMSB(device.getId().getId().getMostSignificantBits()); + relationRequestMsgBuilder.setEntityIdLSB(device.getId().getId().getLeastSignificantBits()); + relationRequestMsgBuilder.setEntityType(device.getId().getEntityType().name()); + testAutoGeneratedCodeByProtobuf(relationRequestMsgBuilder); + + uplinkMsgBuilder.addRelationRequestMsg(relationRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relation.getType(), relationUpdateMsg.getType()); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getFromEntityType(), fromUUID); + Assert.assertEquals(relation.getFrom(), fromEntityId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getToEntityType(), toUUID); + Assert.assertEquals(relation.getTo(), toEntityId); + + Assert.assertEquals(relation.getTypeGroup().name(), relationUpdateMsg.getTypeGroup()); + } + + private void sendAlarm() throws Exception { + Device device = findDeviceByName("Edge Device 2"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AlarmUpdateMsg.Builder alarmUpdateMgBuilder = AlarmUpdateMsg.newBuilder(); + alarmUpdateMgBuilder.setName("alarm from edge"); + alarmUpdateMgBuilder.setStatus(AlarmStatus.ACTIVE_UNACK.name()); + alarmUpdateMgBuilder.setSeverity(AlarmSeverity.CRITICAL.name()); + alarmUpdateMgBuilder.setOriginatorName(device.getName()); + alarmUpdateMgBuilder.setOriginatorType(EntityType.DEVICE.name()); + testAutoGeneratedCodeByProtobuf(alarmUpdateMgBuilder); + uplinkMsgBuilder.addAlarmUpdateMsg(alarmUpdateMgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + + + List alarms = doGetTypedWithPageLink("/api/alarm/{entityType}/{entityId}?", + new TypeReference>() {}, + new PageLink(100), device.getId().getEntityType().name(), device.getId().getId().toString()) + .getData(); + Optional foundAlarm = alarms.stream().filter(alarm -> alarm.getType().equals("alarm from edge")).findAny(); + Assert.assertTrue(foundAlarm.isPresent()); + AlarmInfo alarmInfo = foundAlarm.get(); + Assert.assertEquals(device.getId(), alarmInfo.getOriginator()); + Assert.assertEquals(AlarmStatus.ACTIVE_UNACK, alarmInfo.getStatus()); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmInfo.getSeverity()); + } + + private void sendRelation() throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Optional foundDevice1 = edgeDevices.stream().filter(device1 -> device1.getName().equals("Edge Device 1")).findAny(); + Assert.assertTrue(foundDevice1.isPresent()); + Device device1 = foundDevice1.get(); + Optional foundDevice2 = edgeDevices.stream().filter(device2 -> device2.getName().equals("Edge Device 2")).findAny(); + Assert.assertTrue(foundDevice2.isPresent()); + Device device2 = foundDevice2.get(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RelationUpdateMsg.Builder relationUpdateMsgBuilder = RelationUpdateMsg.newBuilder(); + relationUpdateMsgBuilder.setType("test"); + relationUpdateMsgBuilder.setTypeGroup(RelationTypeGroup.COMMON.name()); + relationUpdateMsgBuilder.setToIdMSB(device1.getId().getId().getMostSignificantBits()); + relationUpdateMsgBuilder.setToIdLSB(device1.getId().getId().getLeastSignificantBits()); + relationUpdateMsgBuilder.setToEntityType(device1.getId().getEntityType().name()); + relationUpdateMsgBuilder.setFromIdMSB(device2.getId().getId().getMostSignificantBits()); + relationUpdateMsgBuilder.setFromIdLSB(device2.getId().getId().getLeastSignificantBits()); + relationUpdateMsgBuilder.setFromEntityType(device2.getId().getEntityType().name()); + relationUpdateMsgBuilder.setAdditionalInfo("{}"); + testAutoGeneratedCodeByProtobuf(relationUpdateMsgBuilder); + uplinkMsgBuilder.addRelationUpdateMsg(relationUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + + EntityRelation relation = doGet("/api/relation?" + + "&fromId=" + device2.getId().getId().toString() + + "&fromType=" + device2.getId().getEntityType().name() + + "&relationType=" + "test" + + "&relationTypeGroup=" + RelationTypeGroup.COMMON.name() + + "&toId=" + device1.getId().getId().toString() + + "&toType=" + device1.getId().getEntityType().name(), EntityRelation.class); + Assert.assertNotNull(relation); + } + + private void sendTelemetry() throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Optional foundDevice = edgeDevices.stream().filter(device1 -> device1.getName().equals("Edge Device 2")).findAny(); + Assert.assertTrue(foundDevice.isPresent()); + Device device = foundDevice.get(); + + edgeImitator.expectResponsesAmount(2); + + JsonObject data = new JsonObject(); + String timeseriesKey = "key"; + String timeseriesValue = "25"; + data.addProperty(timeseriesKey, timeseriesValue); + UplinkMsg.Builder uplinkMsgBuilder1 = UplinkMsg.newBuilder(); + EntityDataProto.Builder entityDataBuilder = EntityDataProto.newBuilder(); + entityDataBuilder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data, System.currentTimeMillis())); + entityDataBuilder.setEntityType(device.getId().getEntityType().name()); + entityDataBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityDataBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(entityDataBuilder); + uplinkMsgBuilder1.addEntityData(entityDataBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder1.build()); + + JsonObject attributesData = new JsonObject(); + String attributesKey = "test_attr"; + String attributesValue = "test_value"; + attributesData.addProperty(attributesKey, attributesValue); + UplinkMsg.Builder uplinkMsgBuilder2 = UplinkMsg.newBuilder(); + EntityDataProto.Builder entityDataBuilder2 = EntityDataProto.newBuilder(); + entityDataBuilder2.setEntityType(device.getId().getEntityType().name()); + entityDataBuilder2.setEntityIdMSB(device.getId().getId().getMostSignificantBits()); + entityDataBuilder2.setEntityIdLSB(device.getId().getId().getLeastSignificantBits()); + entityDataBuilder2.setAttributesUpdatedMsg(JsonConverter.convertToAttributesProto(attributesData)); + entityDataBuilder2.setPostAttributeScope(DataConstants.SERVER_SCOPE); + testAutoGeneratedCodeByProtobuf(entityDataBuilder2); + + uplinkMsgBuilder2.addEntityData(entityDataBuilder2.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder2); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder2.build()); + edgeImitator.waitForResponses(); + + // Wait before device attributes saved to database before requesting them from controller + Thread.sleep(1000); + Map>> timeseries = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/values/timeseries?keys=" + timeseriesKey, new TypeReference<>() {}); + Assert.assertTrue(timeseries.containsKey(timeseriesKey)); + Assert.assertEquals(1, timeseries.get(timeseriesKey).size()); + Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value")); + + List> attributes = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/" + DataConstants.SERVER_SCOPE, new TypeReference<>() {}); + Assert.assertEquals(1, attributes.size()); + Assert.assertEquals(attributes.get(0).get("key"), attributesKey); + Assert.assertEquals(attributes.get(0).get("value"), attributesValue); + + } + + private void sendRuleChainMetadataRequest() throws Exception { + RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder(); + ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits()); + ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; + Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), edgeRootRuleChainId.getId().getMostSignificantBits()); + Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdLSB(), edgeRootRuleChainId.getId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg); + } + + private void sendUserCredentialsRequest() throws Exception { + UserId userId = edgeImitator.getUserId(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); + userCredentialsRequestMsgBuilder.setUserIdMSB(userId.getId().getMostSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdLSB(userId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); + uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); + UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdMSB(), userId.getId().getMostSignificantBits()); + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdLSB(), userId.getId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); + } + + private void sendDeviceCredentialsRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + DeviceCredentials deviceCredentials = doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceCredentialsRequestMsg.Builder deviceCredentialsRequestMsgBuilder = DeviceCredentialsRequestMsg.newBuilder(); + deviceCredentialsRequestMsgBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceCredentialsRequestMsgBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(deviceCredentialsRequestMsgBuilder); + uplinkMsgBuilder.addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; + Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsType(), deviceCredentials.getCredentialsType().name()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentials.getCredentialsId()); + } + + private void sendDeviceCredentialsUpdate() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceCredentialsUpdateMsg.Builder deviceCredentialsUpdateMsgBuilder = DeviceCredentialsUpdateMsg.newBuilder(); + deviceCredentialsUpdateMsgBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceCredentialsUpdateMsgBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + deviceCredentialsUpdateMsgBuilder.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN.name()); + deviceCredentialsUpdateMsgBuilder.setCredentialsId("NEW_TOKEN"); + testAutoGeneratedCodeByProtobuf(deviceCredentialsUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + } + + private void sendDeviceRpcResponse() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceRpcCallMsg.Builder deviceRpcCallResponseBuilder = DeviceRpcCallMsg.newBuilder(); + deviceRpcCallResponseBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceRpcCallResponseBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + deviceRpcCallResponseBuilder.setOneway(true); + deviceRpcCallResponseBuilder.setRequestId(0); + deviceRpcCallResponseBuilder.setExpirationTime(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10)); + RpcResponseMsg.Builder responseBuilder = + RpcResponseMsg.newBuilder().setResponse("{}"); + testAutoGeneratedCodeByProtobuf(responseBuilder); + + deviceRpcCallResponseBuilder.setResponseMsg(responseBuilder.build()); + testAutoGeneratedCodeByProtobuf(deviceRpcCallResponseBuilder); + + uplinkMsgBuilder.addDeviceRpcCallMsg(deviceRpcCallResponseBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + } + + private void sendAttributesRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + sendAttributesRequest(device, DataConstants.SERVER_SCOPE, "{\"key1\":\"value1\"}", "key1", "value1"); + sendAttributesRequest(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}", "key2", "value2"); + } + + private void sendAttributesRequest(Device device, String scope, String attributesDataStr, String expectedKey, String expectedValue) throws Exception { + JsonNode attributesData = mapper.readTree(attributesDataStr); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getId().getId().toString() + "/attributes/" + scope, + attributesData); + + // Wait before device attributes saved to database before requesting them from edge + Thread.sleep(1000); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AttributesRequestMsg.Builder attributesRequestMsgBuilder = AttributesRequestMsg.newBuilder(); + attributesRequestMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + attributesRequestMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + attributesRequestMsgBuilder.setEntityType(EntityType.DEVICE.name()); + attributesRequestMsgBuilder.setScope(scope); + testAutoGeneratedCodeByProtobuf(attributesRequestMsgBuilder); + uplinkMsgBuilder.addAttributesRequestMsg(attributesRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + edgeImitator.waitForMessages(); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals(scope, latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasAttributesUpdatedMsg()); + + TransportProtos.PostAttributeMsg attributesUpdatedMsg = latestEntityDataMsg.getAttributesUpdatedMsg(); + Assert.assertEquals(1, attributesUpdatedMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = attributesUpdatedMsg.getKv(0); + Assert.assertEquals(expectedKey, keyValueProto.getKey()); + Assert.assertEquals(expectedValue, keyValueProto.getStringV()); + } + + private void sendDeleteDeviceOnEdge() throws Exception { + Device device = findDeviceByName("Edge Device 2"); + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + deviceDeleteMsgBuilder.setIdMSB(device.getId().getId().getMostSignificantBits()); + deviceDeleteMsgBuilder.setIdLSB(device.getId().getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder); + + upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + edgeImitator.waitForResponses(); + device = doGet("/api/device/" + device.getId().getId().toString(), Device.class); + Assert.assertNotNull(device); + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeDevices.contains(device)); + } + + private void installation() throws Exception { + edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + + Device savedDevice = saveDevice("Edge Device 1"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + Asset savedAsset = saveAsset("Edge Asset 1"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + } + + private EdgeEvent constructEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventActionType edgeEventAction, UUID entityId, EdgeEventType edgeEventType, JsonNode entityBody) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(edgeEventType); + edgeEvent.setBody(entityBody); + return edgeEvent; + } + + private void testAutoGeneratedCodeByProtobuf(MessageLite.Builder builder) throws InvalidProtocolBufferException { + MessageLite source = builder.build(); + + testAutoGeneratedCodeByProtobuf(source); + + MessageLite target = source.getParserForType().parseFrom(source.toByteArray()); + builder.clear().mergeFrom(target); + } + + private void testAutoGeneratedCodeByProtobuf(MessageLite source) throws InvalidProtocolBufferException { + MessageLite target = source.getParserForType().parseFrom(source.toByteArray()); + Assert.assertEquals(source, target); + Assert.assertEquals(source.hashCode(), target.hashCode()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java new file mode 100644 index 0000000000..19b6bd7f23 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2021 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.edge; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.runner.RunWith; +import org.thingsboard.server.dao.CustomSqlUnit; +import org.thingsboard.server.queue.memory.InMemoryStorage; + +import java.util.Arrays; + +@RunWith(ClasspathSuite.class) +@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.edge.sql.*Test"}) +public class EdgeSqlTestSuite { + + @ClassRule + public static CustomSqlUnit sqlUnit = new CustomSqlUnit( + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + "sql/hsql/drop-all-tables.sql", + "sql-test.properties"); + + @BeforeClass + public static void cleanupInMemStorage(){ + InMemoryStorage.getInstance().cleanup(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java new file mode 100644 index 0000000000..48ff61a491 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -0,0 +1,296 @@ +/** + * Copyright © 2016-2021 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.edge.imitator; + +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.protobuf.AbstractMessage; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.edge.rpc.EdgeGrpcClient; +import org.thingsboard.edge.rpc.EdgeRpcClient; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Slf4j +public class EdgeImitator { + + private String routingKey; + private String routingSecret; + + private EdgeRpcClient edgeRpcClient; + + private final Lock lock = new ReentrantLock(); + + private CountDownLatch messagesLatch; + private CountDownLatch responsesLatch; + private List> ignoredTypes; + + @Getter + private EdgeConfiguration configuration; + @Getter + private UserId userId; + @Getter + private final List downlinkMsgs; + + public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException { + edgeRpcClient = new EdgeGrpcClient(); + messagesLatch = new CountDownLatch(0); + responsesLatch = new CountDownLatch(0); + downlinkMsgs = new ArrayList<>(); + ignoredTypes = new ArrayList<>(); + this.routingKey = routingKey; + this.routingSecret = routingSecret; + setEdgeCredentials("rpcHost", host); + setEdgeCredentials("rpcPort", port); + setEdgeCredentials("keepAliveTimeSec", 300); + } + + private void setEdgeCredentials(String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { + Field fieldToSet = edgeRpcClient.getClass().getDeclaredField(fieldName); + fieldToSet.setAccessible(true); + fieldToSet.set(edgeRpcClient, value); + fieldToSet.setAccessible(false); + } + + public void connect() { + edgeRpcClient.connect(routingKey, routingSecret, + this::onUplinkResponse, + this::onEdgeUpdate, + this::onDownlink, + this::onClose); + + edgeRpcClient.sendSyncRequestMsg(); + } + + public void disconnect() throws InterruptedException { + edgeRpcClient.disconnect(false); + } + + public void sendUplinkMsg(UplinkMsg uplinkMsg) { + edgeRpcClient.sendUplinkMsg(uplinkMsg); + } + + private void onUplinkResponse(UplinkResponseMsg msg) { + log.info("onUplinkResponse: {}", msg); + responsesLatch.countDown(); + } + + private void onEdgeUpdate(EdgeConfiguration edgeConfiguration) { + this.configuration = edgeConfiguration; + } + + private void onUserUpdate(UserUpdateMsg userUpdateMsg) { + this.userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB())); + } + + private void onDownlink(DownlinkMsg downlinkMsg) { + ListenableFuture> future = processDownlinkMsg(downlinkMsg); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder().setSuccess(true).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); + } + + @Override + public void onFailure(Throwable t) { + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder().setSuccess(false).setErrorMsg(t.getMessage()).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); + } + }, MoreExecutors.directExecutor()); + } + + private void onClose(Exception e) { + log.info("onClose: {}", e.getMessage()); + } + + private ListenableFuture> processDownlinkMsg(DownlinkMsg downlinkMsg) { + List> result = new ArrayList<>(); + if (downlinkMsg.getDeviceUpdateMsgList() != null && !downlinkMsg.getDeviceUpdateMsgList().isEmpty()) { + for (DeviceUpdateMsg deviceUpdateMsg: downlinkMsg.getDeviceUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceUpdateMsg)); + } + } + if (downlinkMsg.getDeviceCredentialsUpdateMsgList() != null && !downlinkMsg.getDeviceCredentialsUpdateMsgList().isEmpty()) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg: downlinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg)); + } + } + if (downlinkMsg.getAssetUpdateMsgList() != null && !downlinkMsg.getAssetUpdateMsgList().isEmpty()) { + for (AssetUpdateMsg assetUpdateMsg: downlinkMsg.getAssetUpdateMsgList()) { + result.add(saveDownlinkMsg(assetUpdateMsg)); + } + } + if (downlinkMsg.getRuleChainUpdateMsgList() != null && !downlinkMsg.getRuleChainUpdateMsgList().isEmpty()) { + for (RuleChainUpdateMsg ruleChainUpdateMsg: downlinkMsg.getRuleChainUpdateMsgList()) { + result.add(saveDownlinkMsg(ruleChainUpdateMsg)); + } + } + if (downlinkMsg.getRuleChainMetadataUpdateMsgList() != null && !downlinkMsg.getRuleChainMetadataUpdateMsgList().isEmpty()) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg: downlinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(saveDownlinkMsg(ruleChainMetadataUpdateMsg)); + } + } + if (downlinkMsg.getDashboardUpdateMsgList() != null && !downlinkMsg.getDashboardUpdateMsgList().isEmpty()) { + for (DashboardUpdateMsg dashboardUpdateMsg: downlinkMsg.getDashboardUpdateMsgList()) { + result.add(saveDownlinkMsg(dashboardUpdateMsg)); + } + } + if (downlinkMsg.getRelationUpdateMsgList() != null && !downlinkMsg.getRelationUpdateMsgList().isEmpty()) { + for (RelationUpdateMsg relationUpdateMsg: downlinkMsg.getRelationUpdateMsgList()) { + result.add(saveDownlinkMsg(relationUpdateMsg)); + } + } + if (downlinkMsg.getAlarmUpdateMsgList() != null && !downlinkMsg.getAlarmUpdateMsgList().isEmpty()) { + for (AlarmUpdateMsg alarmUpdateMsg: downlinkMsg.getAlarmUpdateMsgList()) { + result.add(saveDownlinkMsg(alarmUpdateMsg)); + } + } + if (downlinkMsg.getEntityDataList() != null && !downlinkMsg.getEntityDataList().isEmpty()) { + for (EntityDataProto entityData: downlinkMsg.getEntityDataList()) { + result.add(saveDownlinkMsg(entityData)); + } + } + if (downlinkMsg.getEntityViewUpdateMsgList() != null && !downlinkMsg.getEntityViewUpdateMsgList().isEmpty()) { + for (EntityViewUpdateMsg entityViewUpdateMsg: downlinkMsg.getEntityViewUpdateMsgList()) { + result.add(saveDownlinkMsg(entityViewUpdateMsg)); + } + } + if (downlinkMsg.getCustomerUpdateMsgList() != null && !downlinkMsg.getCustomerUpdateMsgList().isEmpty()) { + for (CustomerUpdateMsg customerUpdateMsg: downlinkMsg.getCustomerUpdateMsgList()) { + result.add(saveDownlinkMsg(customerUpdateMsg)); + } + } + if (downlinkMsg.getWidgetsBundleUpdateMsgList() != null && !downlinkMsg.getWidgetsBundleUpdateMsgList().isEmpty()) { + for (WidgetsBundleUpdateMsg widgetsBundleUpdateMsg: downlinkMsg.getWidgetsBundleUpdateMsgList()) { + result.add(saveDownlinkMsg(widgetsBundleUpdateMsg)); + } + } + if (downlinkMsg.getWidgetTypeUpdateMsgList() != null && !downlinkMsg.getWidgetTypeUpdateMsgList().isEmpty()) { + for (WidgetTypeUpdateMsg widgetTypeUpdateMsg: downlinkMsg.getWidgetTypeUpdateMsgList()) { + result.add(saveDownlinkMsg(widgetTypeUpdateMsg)); + } + } + if (downlinkMsg.getUserUpdateMsgList() != null && !downlinkMsg.getUserUpdateMsgList().isEmpty()) { + for (UserUpdateMsg userUpdateMsg: downlinkMsg.getUserUpdateMsgList()) { + onUserUpdate(userUpdateMsg); + result.add(saveDownlinkMsg(userUpdateMsg)); + } + } + if (downlinkMsg.getUserCredentialsUpdateMsgList() != null && !downlinkMsg.getUserCredentialsUpdateMsgList().isEmpty()) { + for (UserCredentialsUpdateMsg userCredentialsUpdateMsg: downlinkMsg.getUserCredentialsUpdateMsgList()) { + result.add(saveDownlinkMsg(userCredentialsUpdateMsg)); + } + } + if (downlinkMsg.getDeviceRpcCallMsgList() != null && !downlinkMsg.getDeviceRpcCallMsgList().isEmpty()) { + for (DeviceRpcCallMsg deviceRpcCallMsg: downlinkMsg.getDeviceRpcCallMsgList()) { + result.add(saveDownlinkMsg(deviceRpcCallMsg)); + } + } + if (downlinkMsg.getDeviceCredentialsRequestMsgList() != null && !downlinkMsg.getDeviceCredentialsRequestMsgList().isEmpty()) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg: downlinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(saveDownlinkMsg(deviceCredentialsRequestMsg)); + } + } + return Futures.allAsList(result); + } + + private ListenableFuture saveDownlinkMsg(AbstractMessage message) { + if (!ignoredTypes.contains(message.getClass())) { + try { + lock.lock(); + downlinkMsgs.add(message); + } finally { + lock.unlock(); + } + messagesLatch.countDown(); + } + return Futures.immediateFuture(null); + } + + public void waitForMessages() throws InterruptedException { + messagesLatch.await(5, TimeUnit.SECONDS); + } + + public void expectMessageAmount(int messageAmount) { + messagesLatch = new CountDownLatch(messageAmount); + } + + public void waitForResponses() throws InterruptedException { responsesLatch.await(5, TimeUnit.SECONDS); } + + public void expectResponsesAmount(int messageAmount) { + responsesLatch = new CountDownLatch(messageAmount); + } + + public Optional findMessageByType(Class tClass) { + Optional result; + try { + lock.lock(); + result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).findAny(); + } finally { + lock.unlock(); + } + return result; + } + + public AbstractMessage getLatestMessage() { + return downlinkMsgs.get(downlinkMsgs.size() - 1); + } + + public void ignoreType(Class type) { + ignoredTypes.add(type); + } + + public void allowIgnoredTypes() { + ignoredTypes.clear(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java b/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java new file mode 100644 index 0000000000..041023d0b1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.edge.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edge.BaseEdgeTest; + +@DaoSqlTest +public class EdgeSqlTest extends BaseEdgeTest { +} diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml new file mode 100644 index 0000000000..8adbe4329a --- /dev/null +++ b/common/coap-server/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + coap-server + jar + + Thingsboard CoAP server + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + queue + + + org.thingsboard.common + data + + + org.thingsboard.common.transport + transport-api + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.eclipse.californium + californium-core + + + org.eclipse.californium + scandium + + + + + \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java new file mode 100644 index 0000000000..4129108ba3 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +@Slf4j +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@Component +public class CoapServerContext { + + @Getter + @Value("${transport.coap.bind_address}") + private String host; + + @Getter + @Value("${transport.coap.bind_port}") + private Integer port; + + @Getter + @Value("${transport.coap.timeout}") + private Long timeout; + + @Getter + @Autowired(required = false) + private TbCoapDtlsSettings dtlsSettings; + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java new file mode 100644 index 0000000000..f8b3ffefc2 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import org.eclipse.californium.core.CoapServer; + +import java.net.UnknownHostException; +import java.util.concurrent.ConcurrentMap; + +public interface CoapServerService { + + CoapServer getCoapServer() throws UnknownHostException; + + ConcurrentMap getDtlsSessionsMap(); + + long getTimeout(); + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java new file mode 100644 index 0000000000..aebac1a86d --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +public class DefaultCoapServerService implements CoapServerService { + + @Autowired + private CoapServerContext coapServerContext; + + private CoapServer server; + + private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + + private ScheduledExecutorService dtlsSessionsExecutor; + + @PostConstruct + public void init() throws UnknownHostException { + createCoapServer(); + } + + @PreDestroy + public void shutdown() { + if (dtlsSessionsExecutor != null) { + dtlsSessionsExecutor.shutdownNow(); + } + log.info("Stopping CoAP server!"); + server.destroy(); + log.info("CoAP server stopped!"); + } + + @Override + public CoapServer getCoapServer() throws UnknownHostException { + if (server != null) { + return server; + } else { + return createCoapServer(); + } + } + + @Override + public ConcurrentMap getDtlsSessionsMap() { + return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null; + } + + @Override + public long getTimeout() { + return coapServerContext.getTimeout(); + } + + private CoapServer createCoapServer() throws UnknownHostException { + server = new CoapServer(); + + CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); + InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); + InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); + noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr); + noSecCoapEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard()); + CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); + server.addEndpoint(noSecCoapEndpoint); + + if (isDtlsEnabled()) { + CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); + TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); + DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(); + DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); + dtlsCoapEndpointBuilder.setConnector(connector); + CoapEndpoint dtlsCoapEndpoint = dtlsCoapEndpointBuilder.build(); + server.addEndpoint(dtlsCoapEndpoint); + if (dtlsConnectorConfig.isClientAuthenticationRequired()) { + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor(); + dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + } + } + Resource root = server.getRoot(); + TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); + server.setMessageDeliverer(messageDeliverer); + + server.start(); + return server; + } + + private boolean isDtlsEnabled() { + return coapServerContext.getDtlsSettings() != null; + } + + private void evictTimeoutSessions() { + tbDtlsCertificateVerifier.evictTimeoutSessions(); + } + + private long getDtlsSessionReportTimeout() { + return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); + } + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java new file mode 100644 index 0000000000..99a35d4831 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java @@ -0,0 +1,161 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.CertPathUtil; +import org.eclipse.californium.scandium.dtls.AlertMessage; +import org.eclipse.californium.scandium.dtls.CertificateMessage; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.CertificateVerificationResult; +import org.eclipse.californium.scandium.dtls.ConnectionId; +import org.eclipse.californium.scandium.dtls.DTLSSession; +import org.eclipse.californium.scandium.dtls.HandshakeException; +import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; +import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; +import org.eclipse.californium.scandium.util.ServerNames; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.util.SslUtil; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import javax.security.auth.x500.X500Principal; +import java.security.cert.CertPath; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVerifier { + + private final TbCoapDtlsSessionInMemoryStorage tbCoapDtlsSessionInMemoryStorage; + + private TransportService transportService; + private TbServiceInfoProvider serviceInfoProvider; + private boolean skipValidityCheckForClientCert; + + public TbCoapDtlsCertificateVerifier(TransportService transportService, TbServiceInfoProvider serviceInfoProvider, long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout, boolean skipValidityCheckForClientCert) { + this.transportService = transportService; + this.serviceInfoProvider = serviceInfoProvider; + this.skipValidityCheckForClientCert = skipValidityCheckForClientCert; + this.tbCoapDtlsSessionInMemoryStorage = new TbCoapDtlsSessionInMemoryStorage(dtlsSessionInactivityTimeout, dtlsSessionReportTimeout); + } + + @Override + public List getSupportedCertificateType() { + return Collections.singletonList(CertificateType.X_509); + } + + @Override + public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, Boolean clientUsage, boolean truncateCertificatePath, CertificateMessage message, DTLSSession session) { + try { + String credentialsBody = null; + CertPath certpath = message.getCertificateChain(); + X509Certificate[] chain = certpath.getCertificates().toArray(new X509Certificate[0]); + for (X509Certificate cert : chain) { + try { + if (!skipValidityCheckForClientCert) { + cert.checkValidity(); + } + String strCert = SslUtil.getCertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + final ValidateDeviceCredentialsResponse[] deviceCredentialsResponse = new ValidateDeviceCredentialsResponse[1]; + CountDownLatch latch = new CountDownLatch(1); + transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + deviceCredentialsResponse[0] = msg; + } + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + ValidateDeviceCredentialsResponse msg = deviceCredentialsResponse[0]; + if (msg != null && strCert.equals(msg.getCredentials())) { + credentialsBody = msg.getCredentials(); + DeviceProfile deviceProfile = msg.getDeviceProfile(); + if (msg.hasDeviceInfo() && deviceProfile != null) { + TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, serviceInfoProvider.getServiceId(), UUID.randomUUID()); + tbCoapDtlsSessionInMemoryStorage.put(session.getSessionIdentifier().toString(), new TbCoapDtlsSessionInfo(sessionInfoProto, deviceProfile)); + } + break; + } + } catch (InterruptedException | + CertificateEncodingException | + CertificateExpiredException | + CertificateNotYetValidException e) { + log.error(e.getMessage(), e); + } + } + if (credentialsBody == null) { + AlertMessage alert = new AlertMessage(AlertMessage.AlertLevel.FATAL, AlertMessage.AlertDescription.BAD_CERTIFICATE, + session.getPeer()); + throw new HandshakeException("Certificate chain could not be validated", alert); + } else { + return new CertificateVerificationResult(cid, certpath, null); + } + } catch (HandshakeException e) { + log.trace("Certificate validation failed!", e); + return new CertificateVerificationResult(cid, e, null); + } + } + + @Override + public List getAcceptedIssuers() { + return CertPathUtil.toSubjects(null); + } + + @Override + public void setResultHandler(HandshakeResultHandler resultHandler) { + // empty implementation + } + + public ConcurrentMap getTbCoapDtlsSessionIdsMap() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionIdMap(); + } + + public void evictTimeoutSessions() { + tbCoapDtlsSessionInMemoryStorage.evictTimeoutSessions(); + } + + public long getDtlsSessionReportTimeout() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionReportTimeout(); + } +} \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java new file mode 100644 index 0000000000..618a10a1eb --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +@Data +public class TbCoapDtlsSessionInMemoryStorage { + + private final ConcurrentMap dtlsSessionIdMap = new ConcurrentHashMap<>(); + private long dtlsSessionInactivityTimeout; + private long dtlsSessionReportTimeout; + + + public TbCoapDtlsSessionInMemoryStorage(long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout) { + this.dtlsSessionInactivityTimeout = dtlsSessionInactivityTimeout; + this.dtlsSessionReportTimeout = dtlsSessionReportTimeout; + } + + public void put(String dtlsSessionId, TbCoapDtlsSessionInfo dtlsSessionInfo) { + log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", dtlsSessionId, dtlsSessionInfo.getLastActivityTime()); + dtlsSessionIdMap.putIfAbsent(dtlsSessionId, dtlsSessionInfo); + } + + public void evictTimeoutSessions() { + long expTime = System.currentTimeMillis() - dtlsSessionInactivityTimeout; + dtlsSessionIdMap.entrySet().removeIf(entry -> { + if (entry.getValue().getLastActivityTime() < expTime) { + log.trace("DTLS session was removed from in-memory store: [{}]", entry.getKey()); + return true; + } else { + return false; + } + }); + } + +} \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java new file mode 100644 index 0000000000..893ca38a5c --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; + +@Data +public class TbCoapDtlsSessionInfo { + + private TransportProtos.SessionInfoProto sessionInfoProto; + private DeviceProfile deviceProfile; + private long lastActivityTime; + + + public TbCoapDtlsSessionInfo(TransportProtos.SessionInfoProto sessionInfoProto, DeviceProfile deviceProfile) { + this.sessionInfoProto = sessionInfoProto; + this.deviceProfile = deviceProfile; + this.lastActivityTime = System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java new file mode 100644 index 0000000000..417a78da12 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2021 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.coapserver; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.Optional; + +@Slf4j +@ConditionalOnExpression("'${transport.coap.enabled}'=='true'") +@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false) +@Component +public class TbCoapDtlsSettings { + + @Value("${transport.coap.dtls.bind_address}") + private String host; + + @Value("${transport.coap.dtls.bind_port}") + private Integer port; + + @Value("${transport.coap.dtls.mode}") + private String mode; + + @Value("${transport.coap.dtls.key_store}") + private String keyStoreFile; + + @Value("${transport.coap.dtls.key_store_password}") + private String keyStorePassword; + + @Value("${transport.coap.dtls.key_password}") + private String keyPassword; + + @Value("${transport.coap.dtls.key_alias}") + private String keyAlias; + + @Value("${transport.coap.dtls.skip_validity_check_for_client_cert}") + private boolean skipValidityCheckForClientCert; + + @Value("${transport.coap.dtls.x509.dtls_session_inactivity_timeout}") + private long dtlsSessionInactivityTimeout; + + @Value("${transport.coap.dtls.x509.dtls_session_report_timeout}") + private long dtlsSessionReportTimeout; + + @Autowired + private TransportService transportService; + + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException { + Optional securityModeOpt = SecurityMode.parse(mode); + if (securityModeOpt.isEmpty()) { + log.warn("Incorrect configuration of securityMode {}", mode); + throw new RuntimeException("Failed to parse mode property: " + mode + "!"); + } else { + DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(); + configBuilder.setAddress(getInetSocketAddress()); + String keyStoreFilePath = Resources.getResource(keyStoreFile).getPath(); + SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath); + SecurityMode securityMode = securityModeOpt.get(); + if (securityMode.equals(SecurityMode.NO_AUTH)) { + configBuilder.setClientAuthenticationRequired(false); + configBuilder.setServerOnly(true); + } else { + configBuilder.setAdvancedCertificateVerifier( + new TbCoapDtlsCertificateVerifier( + transportService, + serviceInfoProvider, + dtlsSessionInactivityTimeout, + dtlsSessionReportTimeout, + skipValidityCheckForClientCert + ) + ); + } + configBuilder.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), + Collections.singletonList(CertificateType.X_509)); + return configBuilder.build(); + } + } + + private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) { + try { + return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(), + keyPassword.toCharArray()); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load serverCredentials due to: ", e); + } + } + + private void loadTrustedCertificates(DtlsConnectorConfig.Builder config, String keyStoreFilePath) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreFilePath, keyAlias, + keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + if (trustBuilder.hasTrusts()) { + config.setAdvancedCertificateVerifier(trustBuilder.build()); + } + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load trusted certificates due to: ", e); + } + } + + private InetSocketAddress getInetSocketAddress() throws UnknownHostException { + InetAddress addr = InetAddress.getByName(host); + return new InetSocketAddress(addr, port); + } + + private enum SecurityMode { + X509, + NO_AUTH; + + static Optional parse(String name) { + SecurityMode mode = null; + if (name != null) { + for (SecurityMode securityMode : SecurityMode.values()) { + if (securityMode.name().equalsIgnoreCase(name)) { + mode = securityMode; + break; + } + } + } + return Optional.ofNullable(mode); + } + + } + +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java similarity index 97% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java index fa189e35f1..307455a6a5 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.OptionSet; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index a0b44775f5..0280a81f1a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -22,9 +22,11 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; import java.util.Optional; @@ -74,4 +76,12 @@ public interface AssetService { ListenableFuture> findAssetsByQuery(TenantId tenantId, AssetSearchQuery query); ListenableFuture> findAssetTypesByTenantId(TenantId tenantId); + + Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId); + + Asset unassignAssetFromEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId); + + PageData findAssetsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findAssetsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 62c7e02e2f..fc3eaf9a12 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -20,10 +20,10 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; public interface DashboardService { @@ -53,4 +53,9 @@ public interface DashboardService { void updateCustomerDashboards(TenantId tenantId, CustomerId customerId); + Dashboard assignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId); + + Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId); + + PageData findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 1a893f5388..6639b75468 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -93,4 +94,12 @@ public interface DeviceService { Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); List findDevicesIdsByDeviceProfileTransportType(DeviceTransportType transportType); + + Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId); + + Device unassignDeviceFromEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId); + + PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java new file mode 100644 index 0000000000..e1f3203bc6 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +public interface EdgeEventService { + + ListenableFuture saveAsync(EdgeEvent edgeEvent); + + PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java new file mode 100644 index 0000000000..a7b9145c01 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.util.List; +import java.util.Optional; + +public interface EdgeService { + + Edge findEdgeById(TenantId tenantId, EdgeId edgeId); + + EdgeInfo findEdgeInfoById(TenantId tenantId, EdgeId edgeId); + + ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId); + + Edge findEdgeByTenantIdAndName(TenantId tenantId, String name); + + Optional findEdgeByRoutingKey(TenantId tenantId, String routingKey); + + Edge saveEdge(Edge edge); + + Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId); + + Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId); + + void deleteEdge(TenantId tenantId, EdgeId edgeId); + + PageData findEdgesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findEdgesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantId(TenantId tenantId, PageLink pageLink); + + ListenableFuture> findEdgesByTenantIdAndIdsAsync(TenantId tenantId, List edgeIds); + + void deleteEdgesByTenantId(TenantId tenantId); + + PageData findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findEdgesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List edgeIds); + + void unassignCustomerEdges(TenantId tenantId, CustomerId customerId); + + ListenableFuture> findEdgesByQuery(TenantId tenantId, EdgeSearchQuery query); + + ListenableFuture> findEdgeTypesByTenantId(TenantId tenantId); + + void assignDefaultRuleChainsToEdge(TenantId tenantId, EdgeId edgeId); + + ListenableFuture> findEdgesByTenantIdAndRuleChainId(TenantId tenantId, RuleChainId ruleChainId); + + ListenableFuture> findEdgesByTenantIdAndDashboardId(TenantId tenantId, DashboardId dashboardId); + + ListenableFuture> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId); + + Object checkInstance(Object request); + + Object activateInstance(String licenseSecret, String releaseDate); + + String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index bfa407c071..832a7822ee 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -19,14 +19,15 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; @@ -76,4 +77,12 @@ public interface EntityViewService { void deleteEntityViewsByTenantId(TenantId tenantId); ListenableFuture> findEntityViewTypesByTenantId(TenantId tenantId); + + EntityView assignEntityViewToEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId); + + EntityView unassignEntityViewFromEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId); + + PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index aff0fbc3ac..086d954e27 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -17,16 +17,19 @@ package org.thingsboard.server.dao.rule; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import java.util.List; @@ -60,7 +63,7 @@ public interface RuleChainService { List getRuleNodeRelations(TenantId tenantId, RuleNodeId ruleNodeId); - PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink); + PageData findTenantRuleChainsByType(TenantId tenantId, RuleChainType type, PageLink pageLink); void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId); @@ -68,6 +71,22 @@ public interface RuleChainService { RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException; - List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite); + List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite); + + RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId); + + RuleChain unassignRuleChainFromEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId, boolean remove); + + PageData findRuleChainsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + RuleChain getEdgeTemplateRootRuleChain(TenantId tenantId); + + boolean setEdgeTemplateRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + boolean setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + boolean unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 29f20cf861..15741da266 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -36,6 +36,14 @@ + + javax.validation + validation-api + + + org.owasp.antisamy + antisamy + org.slf4j slf4j-api diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java index 9389a2f3d6..356af5dab7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java @@ -18,11 +18,13 @@ package org.thingsboard.server.common.data; import org.thingsboard.server.common.data.id.AdminSettingsId; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; public class AdminSettings extends BaseData { private static final long serialVersionUID = -7670322981725511892L; - + + @NoXss private String key; private transient JsonNode jsonValue; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 62f625f285..e587f611c5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -22,6 +22,7 @@ public class CacheConstants { public static final String SESSIONS_CACHE = "sessions"; public static final String ASSET_CACHE = "assets"; public static final String ENTITY_VIEW_CACHE = "entityViews"; + public static final String EDGE_CACHE = "edges"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; 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 9af8ddb736..a333591e53 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 @@ -17,19 +17,28 @@ package org.thingsboard.server.common.data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { private static final long serialVersionUID = 5047448057830660988L; - + + @NoXss protected String country; + @NoXss protected String state; + @NoXss protected String city; + @NoXss protected String address; + @NoXss protected String address2; + @NoXss protected String zip; + @NoXss protected String phone; + @NoXss protected String email; public ContactBased() { 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 e40ab84925..f6f49bb33b 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 @@ -20,13 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; - -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; public class Customer extends ContactBased implements HasTenantId { private static final long serialVersionUID = -1599722990298929275L; - + + @NoXss private String title; private TenantId tenantId; 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 b1699a624c..582df6a56a 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 @@ -20,7 +20,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import java.util.*; +import java.util.HashSet; +import java.util.Set; public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { 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 39fa30b07f..c6a9488c51 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 @@ -70,6 +70,8 @@ public class DataConstants { public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS"; public static final String PROVISION_FAILURE = "PROVISION_FAILURE"; + public static final String ENTITY_ASSIGNED_TO_EDGE = "ENTITY_ASSIGNED_TO_EDGE"; + public static final String ENTITY_UNASSIGNED_FROM_EDGE = "ENTITY_UNASSIGNED_FROM_EDGE"; public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; @@ -91,4 +93,6 @@ public class DataConstants { public static final String USERNAME = "username"; public static final String PASSWORD = "password"; + public static final String EDGE_MSG_SOURCE = "edge"; + public static final String MSG_SOURCE_KEY = "source"; } 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 ad93983cac..2b5f9a9c1d 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 @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -36,8 +37,11 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; + @NoXss private String label; private DeviceProfileId deviceProfileId; private transient DeviceData deviceData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 8b24cefa71..44c1c4b0ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; +import javax.validation.Valid; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -36,17 +38,22 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId { private TenantId tenantId; + @NoXss private String name; + @NoXss private String description; private boolean isDefault; private DeviceProfileType type; private DeviceTransportType transportType; private DeviceProfileProvisionType provisionType; private RuleChainId defaultRuleChainId; + @NoXss private String defaultQueueName; + @Valid private transient DeviceProfileData profileData; @JsonIgnore private byte[] profileDataBytes; + @NoXss private String provisionDeviceKey; public DeviceProfile() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java new file mode 100644 index 0000000000..8b78bcab3a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.edge.EdgeEventType; + +@Slf4j +public final class EdgeUtils { + + private EdgeUtils() { + } + + public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) { + switch (entityType) { + case EDGE: + return EdgeEventType.EDGE; + case DEVICE: + return EdgeEventType.DEVICE; + case DEVICE_PROFILE: + return EdgeEventType.DEVICE_PROFILE; + case ASSET: + return EdgeEventType.ASSET; + case ENTITY_VIEW: + return EdgeEventType.ENTITY_VIEW; + case DASHBOARD: + return EdgeEventType.DASHBOARD; + case USER: + return EdgeEventType.USER; + case RULE_CHAIN: + return EdgeEventType.RULE_CHAIN; + case ALARM: + return EdgeEventType.ALARM; + case TENANT: + return EdgeEventType.TENANT; + case CUSTOMER: + return EdgeEventType.CUSTOMER; + case WIDGETS_BUNDLE: + return EdgeEventType.WIDGETS_BUNDLE; + case WIDGET_TYPE: + return EdgeEventType.WIDGET_TYPE; + default: + log.warn("Unsupported entity type [{}]", entityType); + return null; + } + } +} 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 f289f802d8..1090a0d20f 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,5 @@ 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, API_USAGE_STATE, TB_RESOURCE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, EDGE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 55ec23a9b3..e8b48ee23a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.common.data.validation.NoXss; /** * Created by Victor Basanets on 8/27/2017. @@ -39,7 +40,9 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo private EntityId entityId; private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; private TelemetryEntityView keys; private long startTimeMs; 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 41dc37ec72..b6adf6cf65 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 @@ -20,13 +20,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class Tenant extends ContactBased implements HasTenantId { private static final long serialVersionUID = 8057243243859922101L; - + + @NoXss private String title; + @NoXss private String region; private TenantProfileId tenantProfileId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 3796ec856d..a0fefea6cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -34,7 +35,9 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn @Slf4j public class TenantProfile extends SearchTextBased implements HasName { + @NoXss private String name; + @NoXss private String description; private boolean isDefault; private boolean isolatedTbCore; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 5792d23887..420aff71ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class User extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -35,7 +35,9 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H private CustomerId customerId; private String email; private Authority authority; + @NoXss private String firstName; + @NoXss private String lastName; public User() { 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 6209a11a49..f9d64cb712 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 @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data.asset; -import com.fasterxml.jackson.databind.JsonNode; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; 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.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -29,8 +32,11 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; + @NoXss private String label; public Asset() { 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 4b0d6accc0..2594c73ebe 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 @@ -45,7 +45,9 @@ public enum ActionType { ASSIGNED_FROM_TENANT(false), ASSIGNED_TO_TENANT(false), PROVISION_SUCCESS(false), - PROVISION_FAILURE(false); + PROVISION_FAILURE(false), + ASSIGNED_TO_EDGE(false), // log edge name + UNASSIGNED_FROM_EDGE(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index bbe20bdcaa..90de622fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -17,15 +17,16 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; -import org.thingsboard.server.common.data.query.KeyFilter; +import java.io.Serializable; +import javax.validation.Valid; import java.util.List; -import java.util.concurrent.TimeUnit; @Data @JsonIgnoreProperties(ignoreUnknown = true) -public class AlarmCondition { +public class AlarmCondition implements Serializable { + @Valid private List condition; private AlarmConditionSpec spec; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index 86aafc19e5..96fbd3465f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -18,13 +18,21 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; + +import java.io.Serializable; @Data -public class AlarmConditionFilter { +public class AlarmConditionFilter implements Serializable { + @Valid private AlarmConditionFilterKey key; private EntityKeyValueType valueType; + @NoXss private Object value; + @Valid private KeyFilterPredicate predicate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index 33ee0b0628..258e50a6a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -16,11 +16,15 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serializable; @Data -public class AlarmConditionFilterKey { +public class AlarmConditionFilterKey implements Serializable { private final AlarmConditionKeyType type; + @NoXss private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index f48b5a25e6..1a34894997 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -29,7 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) -public interface AlarmConditionSpec { +public interface AlarmConditionSpec extends Serializable { @JsonIgnore AlarmConditionSpecType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index f04c75d75f..09c9f084cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -16,13 +16,20 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; + +import java.io.Serializable; @Data -public class AlarmRule { +public class AlarmRule implements Serializable { + @Valid private AlarmCondition condition; private AlarmSchedule schedule; // Advanced + @NoXss private String alarmDetails; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java index 7e3517aa34..1d5eceb1aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -28,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) -public interface AlarmSchedule { +public interface AlarmSchedule extends Serializable { AlarmScheduleType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index edee91f877..5a63413b2a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -17,10 +17,10 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; -import java.util.List; +import java.io.Serializable; @Data -public class CustomTimeScheduleItem { +public class CustomTimeScheduleItem implements Serializable { private boolean enabled; private int dayOfWeek; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index 99dc5c8cd4..7a99ccb41e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serializable; +import javax.validation.Valid; import java.util.List; import java.util.TreeMap; @Data -public class DeviceProfileAlarm { +public class DeviceProfileAlarm implements Serializable { private String id; + @NoXss private String alarmType; + @Valid private TreeMap createRules; + @Valid private AlarmRule clearRule; // Hidden in advanced settings diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java index 91a810f0d7..ce019212eb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.DeviceProfileType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -28,7 +30,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) -public interface DeviceProfileConfiguration { +public interface DeviceProfileConfiguration extends Serializable { @JsonIgnore DeviceProfileType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index b709a691b9..f5a438470e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -17,14 +17,17 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import java.io.Serializable; +import javax.validation.Valid; import java.util.List; @Data -public class DeviceProfileData { +public class DeviceProfileData implements Serializable { private DeviceProfileConfiguration configuration; private DeviceProfileTransportConfiguration transportConfiguration; private DeviceProfileProvisionConfiguration provisionConfiguration; + @Valid private List alarms; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 97de41124a..2b0f1b8da1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -31,7 +33,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) -public interface DeviceProfileProvisionConfiguration { +public interface DeviceProfileProvisionConfiguration extends Serializable { String getProvisionDeviceSecret(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java index 957eecd16e..f71861fa52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java @@ -20,7 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.exception.ThingsboardException; + +import java.io.Serializable; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -30,10 +31,11 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"), @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"), - @JsonSubTypes.Type(value = CoapDeviceProfileTransportConfiguration.class, name = "COAP"), @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M"), - @JsonSubTypes.Type(value = SnmpDeviceProfileTransportConfiguration.class, name = "SNMP")}) -public interface DeviceProfileTransportConfiguration { + @JsonSubTypes.Type(value = CoapDeviceProfileTransportConfiguration.class, name = "COAP"), + @JsonSubTypes.Type(value = SnmpDeviceProfileTransportConfiguration.class, name = "SNMP") +}) +public interface DeviceProfileTransportConfiguration extends Serializable { @JsonIgnore DeviceTransportType getType(); 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 new file mode 100644 index 0000000000..939bfb3648 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Setter +public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { + + private static final long serialVersionUID = 4934987555236873728L; + + private TenantId tenantId; + private CustomerId customerId; + private RuleChainId rootRuleChainId; + private String name; + private String type; + private String label; + private String routingKey; + private String secret; + private String edgeLicenseKey; + private String cloudEndpoint; + + public Edge() { + super(); + } + + public Edge(EdgeId id) { + super(id); + } + + public Edge(Edge edge) { + super(edge); + this.tenantId = edge.getTenantId(); + this.customerId = edge.getCustomerId(); + this.rootRuleChainId = edge.getRootRuleChainId(); + this.type = edge.getType(); + this.label = edge.getLabel(); + this.name = edge.getName(); + this.routingKey = edge.getRoutingKey(); + this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java new file mode 100644 index 0000000000..32d63e8973 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +@Data +public class EdgeEvent extends BaseData { + + private TenantId tenantId; + private EdgeId edgeId; + private EdgeEventActionType action; + private UUID entityId; + private String uid; + private EdgeEventType type; + private transient JsonNode body; + + public EdgeEvent() { + super(); + } + + public EdgeEvent(EdgeEventId id) { + super(id); + } + + public EdgeEvent(EdgeEvent event) { + super(event); + } + +} 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 new file mode 100644 index 0000000000..b6664ea57d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 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.edge; + +public enum EdgeEventActionType { + ADDED, + DELETED, + UPDATED, + POST_ATTRIBUTES, + ATTRIBUTES_UPDATED, + ATTRIBUTES_DELETED, + TIMESERIES_UPDATED, + CREDENTIALS_UPDATED, + ASSIGNED_TO_CUSTOMER, + UNASSIGNED_FROM_CUSTOMER, + RELATION_ADD_OR_UPDATE, + RELATION_DELETED, + RPC_CALL, + ALARM_ACK, + ALARM_CLEAR, + ASSIGNED_TO_EDGE, + UNASSIGNED_FROM_EDGE, + CREDENTIALS_REQUEST, + ENTITY_MERGE_REQUEST +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java new file mode 100644 index 0000000000..19a0314b41 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.edge; + +public enum EdgeEventType { + DASHBOARD, + ASSET, + DEVICE, + DEVICE_PROFILE, + ENTITY_VIEW, + ALARM, + RULE_CHAIN, + RULE_CHAIN_METADATA, + EDGE, + USER, + CUSTOMER, + RELATION, + TENANT, + WIDGETS_BUNDLE, + WIDGET_TYPE, + ADMIN_SETTINGS +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java new file mode 100644 index 0000000000..4ba2892574 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.edge; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EdgeId; + +@Data +public class EdgeInfo extends Edge { + + private String customerTitle; + private boolean customerIsPublic; + + public EdgeInfo() { + super(); + } + + public EdgeInfo(EdgeId edgeId) { + super(edgeId); + } + + public EdgeInfo(Edge edge, String customerTitle, boolean customerIsPublic) { + super(edge); + this.customerTitle = customerTitle; + this.customerIsPublic = customerIsPublic; + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java new file mode 100644 index 0000000000..0da7fd08af --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2021 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.edge; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; + +@Data +public class EdgeSearchQuery { + + private RelationsSearchParameters parameters; + private String relationType; + private List edgeTypes; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.EDGE)))); + return query; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index c9d896cc9e..04930d6464 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -28,7 +28,8 @@ public enum ThingsboardErrorCode { BAD_REQUEST_PARAMS(31), ITEM_NOT_FOUND(32), TOO_MANY_REQUESTS(33), - TOO_MANY_UPDATES(34); + TOO_MANY_UPDATES(34), + SUBSCRIPTION_VIOLATION(40); private int errorCode; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java new file mode 100644 index 0000000000..ce352632db --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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 com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class EdgeEventId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EdgeEventId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EdgeEventId fromString(String edgeEventId) { + return new EdgeEventId(UUID.fromString(edgeEventId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java new file mode 100644 index 0000000000..92421c39fe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2021 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 com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class EdgeId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EdgeId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EdgeId fromString(String integrationId) { + return new EdgeId(UUID.fromString(integrationId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.EDGE; + } +} 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 8e52a5b2a7..d7333b4e87 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.id; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import java.util.UUID; @@ -70,8 +71,40 @@ public class EntityIdFactory { return new ApiUsageStateId(uuid); case TB_RESOURCE: return new TbResourceId(uuid); + case EDGE: + return new EdgeId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } + public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { + switch (edgeEventType) { + case CUSTOMER: + return new CustomerId(uuid); + case USER: + return new UserId(uuid); + case DASHBOARD: + return new DashboardId(uuid); + case DEVICE: + return new DeviceId(uuid); + case DEVICE_PROFILE: + return new DeviceProfileId(uuid); + case ASSET: + return new AssetId(uuid); + case ALARM: + return new AlarmId(uuid); + case RULE_CHAIN: + return new RuleChainId(uuid); + case ENTITY_VIEW: + return new EntityViewId(uuid); + case WIDGETS_BUNDLE: + return new WidgetsBundleId(uuid); + case WIDGET_TYPE: + return new WidgetTypeId(uuid); + case EDGE: + return new EdgeId(uuid); + } + throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java index aeff342582..1e0ff70e8a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java @@ -20,6 +20,6 @@ import lombok.Data; @Data public class LwM2mInstance { int id; - LwM2mResource [] resources; + LwM2mResourceObserve[] resources; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java rename to common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java index 9317232b9d..402309ebf3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java @@ -22,7 +22,7 @@ import java.util.stream.Stream; @Data @AllArgsConstructor -public class LwM2mResource { +public class LwM2mResourceObserve { int id; String name; boolean observe; @@ -30,7 +30,7 @@ public class LwM2mResource { boolean telemetry; String keyName; - public LwM2mResource(int id, String name, boolean observe, boolean attribute, boolean telemetry) { + public LwM2mResourceObserve(int id, String name, boolean observe, boolean attribute, boolean telemetry) { this.id = id; this.name = name; this.observe = observe; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 2fa0a4d36a..307447ef9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -18,15 +18,19 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serializable; @Data @RequiredArgsConstructor -public class DynamicValue { +public class DynamicValue implements Serializable { @JsonIgnore private T resolvedValue; private final DynamicValueSourceType sourceType; + @NoXss private final String sourceAttribute; private final boolean inherit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java new file mode 100644 index 0000000000..9424609e57 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class EdgeSearchQueryFilter extends EntitySearchQueryFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.EDGE_SEARCH_QUERY; + } + + private List edgeTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java new file mode 100644 index 0000000000..3158a6f0d3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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.query; + +import lombok.Data; + +@Data +public class EdgeTypeFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.EDGE_TYPE; + } + + private String edgeType; + + private String edgeNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index efdd70ec7b..958b8367ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -32,12 +32,14 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = EntityTypeFilter.class, name = "entityType"), @JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"), @JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"), + @JsonSubTypes.Type(value = EdgeTypeFilter.class, name = "edgeType"), @JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"), @JsonSubTypes.Type(value = ApiUsageStateFilter.class, name = "apiUsageState"), @JsonSubTypes.Type(value = RelationsQueryFilter.class, name = "relationsQuery"), @JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"), @JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"), - @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery")}) + @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"), + @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")}) public interface EntityFilter { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java index 6b590c4695..1a843c7b0f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -23,10 +23,12 @@ public enum EntityFilterType { ASSET_TYPE("assetType"), DEVICE_TYPE("deviceType"), ENTITY_VIEW_TYPE("entityViewType"), + EDGE_TYPE("edgeType"), RELATIONS_QUERY("relationsQuery"), ASSET_SEARCH_QUERY("assetSearchQuery"), DEVICE_SEARCH_QUERY("deviceSearchQuery"), ENTITY_VIEW_SEARCH_QUERY("entityViewSearchQuery"), + EDGE_SEARCH_QUERY("edgeSearchQuery"), API_USAGE_STATE("apiUsageState"); private final String label; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java index 191ef02f11..233af353d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import java.io.Serializable; + @Data -public class EntityKey { +public class EntityKey implements Serializable { private final EntityKeyType type; private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java index 8bb227ab66..aedf516096 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java @@ -20,15 +20,23 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.Getter; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; + +import java.io.Serializable; @Data -public class FilterPredicateValue { +public class FilterPredicateValue implements Serializable { @Getter + @NoXss private final T defaultValue; @Getter + @NoXss private final T userValue; @Getter + @Valid private final DynamicValue dynamicValue; public FilterPredicateValue(T defaultValue) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java index e8cdd20334..1b1ec51314 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import java.io.Serializable; + @Data -public class KeyFilter { +public class KeyFilter implements Serializable { private EntityKey key; private EntityKeyValueType valueType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java index 5d992bf2ca..79f2f0e132 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -28,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = NumericFilterPredicate.class, name = "NUMERIC"), @JsonSubTypes.Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), @JsonSubTypes.Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) -public interface KeyFilterPredicate { +public interface KeyFilterPredicate extends Serializable { @JsonIgnore FilterPredicateType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java index fffe38cd57..d3a09813e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java @@ -17,10 +17,13 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import javax.validation.Valid; + @Data public class StringFilterPredicate implements SimpleKeyFilterPredicate { private StringOperation operation; + @Valid private FilterPredicateValue value; private boolean ignoreCase; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 66b0b32129..7833acc050 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -32,6 +32,7 @@ public class EntityRelation implements Serializable { private static final long serialVersionUID = 2807343040519543363L; + public static final String EDGE_TYPE = "ManagedByEdge"; public static final String CONTAINS_TYPE = "Contains"; public static final String MANAGES_TYPE = "Manages"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java index 5ff5b685aa..c6ca10088d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java @@ -21,6 +21,8 @@ public enum RelationTypeGroup { ALARM, DASHBOARD, RULE_CHAIN, - RULE_NODE + RULE_NODE, + EDGE, + EDGE_AUTO_ASSIGN_RULE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 330b7de82d..da84f15427 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; @Data @EqualsAndHashCode(callSuper = true) @@ -35,11 +36,14 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im private static final long serialVersionUID = -5656679015121935465L; private TenantId tenantId; + @NoXss private String name; + private RuleChainType type; private RuleNodeId firstRuleNodeId; private boolean root; private boolean debugMode; private transient JsonNode configuration; + @JsonIgnore private byte[] configurationBytes; @@ -55,6 +59,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im super(ruleChain); this.tenantId = ruleChain.getTenantId(); this.name = ruleChain.getName(); + this.type = ruleChain.getType(); this.firstRuleNodeId = ruleChain.getFirstRuleNodeId(); this.root = ruleChain.isRoot(); this.setConfiguration(ruleChain.getConfiguration()); @@ -77,5 +82,4 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im public void setConfiguration(JsonNode data) { setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes); } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java new file mode 100644 index 0000000000..938538c132 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 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.rule; + +public enum RuleChainType { + CORE, EDGE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java new file mode 100644 index 0000000000..2ecc737fee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 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.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Constraint(validatedBy = {}) +public @interface NoXss { + String message() default "field value is malformed"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml new file mode 100644 index 0000000000..cf501686a2 --- /dev/null +++ b/common/edge-api/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + edge-api + jar + + Thingsboard Server Remote Edge wrapper + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + data + + + org.thingsboard.common + queue + + + org.thingsboard.common + message + + + com.google.code.gson + gson + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-web + + + io.netty + netty-all + provided + + + com.google.guava + guava + + + io.grpc + grpc-netty + + + netty-transport + io.netty + + + netty-common + io.netty + + + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + com.google.protobuf + protobuf-java + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + + + + thingsboard-repo-deploy + ThingsBoard Repo Deployment + https://repo.thingsboard.io/artifactory/libs-release-public + + + + diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java b/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java new file mode 100644 index 0000000000..edd53f15e4 --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.edge.exception; + +public class EdgeConnectionException extends RuntimeException { + + private static final long serialVersionUID = -4372754681230555723L; + + public EdgeConnectionException(String message) { + super(message); + } + + public EdgeConnectionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java new file mode 100644 index 0000000000..c5099069ee --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -0,0 +1,221 @@ +/** + * Copyright © 2016-2021 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.edge.rpc; + +import com.google.common.io.Resources; +import io.grpc.ManagedChannel; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.edge.exception.EdgeConnectionException; +import org.thingsboard.server.gen.edge.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.ConnectResponseCode; +import org.thingsboard.server.gen.edge.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.RequestMsgType; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; + +import javax.net.ssl.SSLException; +import java.io.File; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Service +@Slf4j +public class EdgeGrpcClient implements EdgeRpcClient { + + @Value("${cloud.rpc.host}") + private String rpcHost; + @Value("${cloud.rpc.port}") + private int rpcPort; + @Value("${cloud.rpc.timeout}") + private int timeoutSecs; + @Value("${cloud.rpc.keep_alive_time_sec}") + private int keepAliveTimeSec; + @Value("${cloud.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${cloud.rpc.ssl.cert}") + private String certResource; + + private ManagedChannel channel; + + private StreamObserver inputStream; + + private static final ReentrantLock uplinkMsgLock = new ReentrantLock(); + + @Override + public void connect(String edgeKey, + String edgeSecret, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError) { + NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS); + if (sslEnabled) { + try { + builder.sslContext(GrpcSslContexts.forClient().trustManager(new File(Resources.getResource(certResource).toURI())).build()); + } catch (URISyntaxException | SSLException e) { + log.error("Failed to initialize channel!", e); + throw new RuntimeException(e); + } + } else { + builder.usePlaintext(); + } + channel = builder.build(); + EdgeRpcServiceGrpc.EdgeRpcServiceStub stub = EdgeRpcServiceGrpc.newStub(channel); + log.info("[{}] Sending a connect request to the TB!", edgeKey); + this.inputStream = stub.handleMsgs(initOutputStream(edgeKey, onUplinkResponse, onEdgeUpdate, onDownlink, onError)); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.CONNECT_RPC_MESSAGE) + .setConnectRequestMsg(ConnectRequestMsg.newBuilder().setEdgeRoutingKey(edgeKey).setEdgeSecret(edgeSecret).build()) + .build()); + } + + private StreamObserver initOutputStream(String edgeKey, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError) { + return new StreamObserver() { + @Override + public void onNext(ResponseMsg responseMsg) { + if (responseMsg.hasConnectResponseMsg()) { + ConnectResponseMsg connectResponseMsg = responseMsg.getConnectResponseMsg(); + if (connectResponseMsg.getResponseCode().equals(ConnectResponseCode.ACCEPTED)) { + log.info("[{}] Configuration received: {}", edgeKey, connectResponseMsg.getConfiguration()); + onEdgeUpdate.accept(connectResponseMsg.getConfiguration()); + } else { + log.error("[{}] Failed to establish the connection! Code: {}. Error message: {}.", edgeKey, connectResponseMsg.getResponseCode(), connectResponseMsg.getErrorMsg()); + try { + EdgeGrpcClient.this.disconnect(true); + } catch (InterruptedException e) { + log.error("[{}] Got interruption during disconnect!", edgeKey, e); + } + onError.accept(new EdgeConnectionException("Failed to establish the connection! Response code: " + connectResponseMsg.getResponseCode().name())); + } + } else if (responseMsg.hasEdgeUpdateMsg()) { + log.debug("[{}] Edge update message received {}", edgeKey, responseMsg.getEdgeUpdateMsg()); + onEdgeUpdate.accept(responseMsg.getEdgeUpdateMsg().getConfiguration()); + } else if (responseMsg.hasUplinkResponseMsg()) { + log.debug("[{}] Uplink response message received {}", edgeKey, responseMsg.getUplinkResponseMsg()); + onUplinkResponse.accept(responseMsg.getUplinkResponseMsg()); + } else if (responseMsg.hasDownlinkMsg()) { + log.debug("[{}] Downlink message received {}", edgeKey, responseMsg.getDownlinkMsg()); + onDownlink.accept(responseMsg.getDownlinkMsg()); + } + } + + @Override + public void onError(Throwable t) { + log.debug("[{}] The rpc session received an error!", edgeKey, t); + try { + EdgeGrpcClient.this.disconnect(true); + } catch (InterruptedException e) { + log.error("[{}] Got interruption during disconnect!", edgeKey, e); + } + onError.accept(new RuntimeException(t)); + } + + @Override + public void onCompleted() { + log.debug("[{}] The rpc session was closed!", edgeKey); + } + }; + } + + @Override + public void disconnect(boolean onError) throws InterruptedException { + if (!onError) { + try { + inputStream.onCompleted(); + } catch (Exception e) { + log.error("Exception during onCompleted", e); + } + } + if (channel != null) { + channel.shutdown(); + int attempt = 0; + do { + try { + channel.awaitTermination(timeoutSecs, TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Channel await termination was interrupted", e); + } + if (attempt > 5) { + log.warn("We had reached maximum of termination attempts. Force closing channel"); + try { + channel.shutdownNow(); + } catch (Exception e) { + log.error("Exception during shutdownNow", e); + } + break; + } + attempt++; + } while (!channel.isTerminated()); + } + } + + @Override + public void sendUplinkMsg(UplinkMsg msg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setUplinkMsg(msg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendSyncRequestMsg() { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setDownlinkResponseMsg(downlinkResponseMsg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } +} diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java new file mode 100644 index 0000000000..b3928819a1 --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 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.edge.rpc; + +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; + +import java.util.function.Consumer; + +public interface EdgeRpcClient { + + void connect(String integrationKey, + String integrationSecret, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError); + + void disconnect(boolean onError) throws InterruptedException; + + void sendSyncRequestMsg(); + + void sendUplinkMsg(UplinkMsg uplinkMsg); + + void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); +} diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto new file mode 100644 index 0000000000..9b50b628fd --- /dev/null +++ b/common/edge-api/src/main/proto/edge.proto @@ -0,0 +1,439 @@ +/** + * Copyright © 2016-2021 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"; + +option java_package = "org.thingsboard.server.gen.edge"; +option java_multiple_files = true; +option java_outer_classname = "EdgeProtos"; + +import "queue.proto"; + +package edge; + +// Interface exported by the ThingsBoard Edge Transport. +service EdgeRpcService { + + rpc handleMsgs(stream RequestMsg) returns (stream ResponseMsg) {} + +} + +/** + * Data Structures; + */ +message RequestMsg { + RequestMsgType msgType = 1; + ConnectRequestMsg connectRequestMsg = 2; + UplinkMsg uplinkMsg = 3; + DownlinkResponseMsg downlinkResponseMsg = 4; +} + +message ResponseMsg { + ConnectResponseMsg connectResponseMsg = 1; + UplinkResponseMsg uplinkResponseMsg = 2; + DownlinkMsg downlinkMsg = 3; + EdgeUpdateMsg edgeUpdateMsg = 4; +} + +enum RequestMsgType { + CONNECT_RPC_MESSAGE = 0; + UPLINK_RPC_MESSAGE = 1; + SYNC_REQUEST_RPC_MESSAGE = 2; +} + +message EdgeUpdateMsg { + EdgeConfiguration configuration = 1; +} + +message ConnectRequestMsg { + string edgeRoutingKey = 1; + string edgeSecret = 2; +} + +enum ConnectResponseCode { + ACCEPTED = 0; + BAD_CREDENTIALS = 1; + SERVER_UNAVAILABLE = 2; +} + +message ConnectResponseMsg { + ConnectResponseCode responseCode = 1; + string errorMsg = 2; + EdgeConfiguration configuration = 3; +} + +message EdgeConfiguration { + int64 edgeIdMSB = 1; + int64 edgeIdLSB = 2; + int64 tenantIdMSB = 3; + int64 tenantIdLSB = 4; + int64 customerIdMSB = 5; + int64 customerIdLSB = 6; + string name = 7; + string type = 8; + string routingKey = 9; + string secret = 10; + string edgeLicenseKey = 11; + string cloudEndpoint = 12; + string additionalInfo = 13; + string cloudType = 14; +} + +enum UpdateMsgType { + ENTITY_CREATED_RPC_MESSAGE = 0; + ENTITY_UPDATED_RPC_MESSAGE = 1; + ENTITY_DELETED_RPC_MESSAGE = 2; + ALARM_ACK_RPC_MESSAGE = 3; + ALARM_CLEAR_RPC_MESSAGE = 4; + ENTITY_MERGE_RPC_MESSAGE = 5; +} + +message EntityDataProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; + transport.PostTelemetryMsg postTelemetryMsg = 4; + transport.PostAttributeMsg postAttributesMsg = 5; + transport.PostAttributeMsg attributesUpdatedMsg = 6; + string postAttributeScope = 7; + AttributeDeleteMsg attributeDeleteMsg = 8; +} + +message AttributeDeleteMsg { + string scope = 1; + repeated string attributeNames = 2; +} + +message RuleChainUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + int64 firstRuleNodeIdMSB = 5; + int64 firstRuleNodeIdLSB = 6; + bool root = 7; + bool debugMode = 8; + string configuration = 9; +} + +message RuleChainMetadataUpdateMsg { + UpdateMsgType msgType = 1; + int64 ruleChainIdMSB = 2; + int64 ruleChainIdLSB = 3; + int32 firstNodeIndex = 4; + repeated RuleNodeProto nodes = 5; + repeated NodeConnectionInfoProto connections = 6; + repeated RuleChainConnectionInfoProto ruleChainConnections = 7; +} + +message RuleNodeProto { + int64 idMSB = 1; + int64 idLSB = 2; + string type = 3; + string name = 4; + bool debugMode = 5; + string configuration = 6; + string additionalInfo = 7; +} + +message NodeConnectionInfoProto { + int32 fromIndex = 1; + int32 toIndex = 2; + string type = 3; +} + +message RuleChainConnectionInfoProto { + int32 fromIndex = 1; + int64 targetRuleChainIdMSB = 2; + int64 targetRuleChainIdLSB = 3; + string type = 4; + string additionalInfo = 5; +} + +message DashboardUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string title = 6; + string configuration = 7; +} + +message DeviceUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + int64 deviceProfileIdMSB = 6; + int64 deviceProfileIdLSB = 7; + string name = 8; + string type = 9; + string label = 10; + string additionalInfo = 11; + string conflictName = 12; +} + +message DeviceProfileUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + string description = 5; + bool default = 6; + string type = 7; + string transportType = 8; + string provisionType = 9; + int64 defaultRuleChainIdMSB = 10; + int64 defaultRuleChainIdLSB = 11; + string defaultQueueName = 12; + bytes profileDataBytes = 13; + string provisionDeviceKey = 14; +} + +message DeviceCredentialsUpdateMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + string credentialsType = 3; + string credentialsId = 4; + string credentialsValue = 5; +} + +message AssetUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string name = 6; + string type = 7; + string label = 8; + string additionalInfo = 9; +} + +message EntityViewUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string name = 6; + string type = 7; + int64 entityIdMSB = 8; + int64 entityIdLSB = 9; + EdgeEntityType entityType = 10; + string additionalInfo = 11; +} + +message AlarmUpdateMsg { + UpdateMsgType msgType = 1; + string name = 2; + string type = 3; + string originatorType = 4; + string originatorName = 5; + string severity = 6; + string status = 7; + int64 startTs = 8; + int64 endTs = 9; + int64 ackTs = 10; + int64 clearTs = 11; + string details = 12; + bool propagate = 13; +} + +message CustomerUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string title = 4; + string country = 5; + string state = 6; + string city = 7; + string address = 8; + string address2 = 9; + string zip = 10; + string phone = 11; + string email = 12; + string additionalInfo = 13; +} + +message RelationUpdateMsg { + UpdateMsgType msgType = 1; + int64 fromIdMSB = 2; + int64 fromIdLSB = 3; + string fromEntityType = 4; + int64 toIdMSB = 5; + int64 toIdLSB = 6; + string toEntityType = 7; + string type = 8; + string typeGroup = 9; + string additionalInfo = 10; +} + +message UserUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string email = 6; + string authority = 7; + string firstName = 8; + string lastName = 9; + string additionalInfo = 10; +} + +message WidgetsBundleUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string title = 4; + string alias = 5; + bytes image = 6; + bool isSystem = 7; +} + +message WidgetTypeUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string bundleAlias = 4; + string alias = 5; + string name = 6; + string descriptorJson = 7; + bool isSystem = 8; +} + +message AdminSettingsUpdateMsg { + bool isSystem = 1; + string key = 2; + string jsonValue = 3; +} + +message UserCredentialsUpdateMsg { + int64 userIdMSB = 1; + int64 userIdLSB = 2; + bool enabled = 3; + string password = 4; +} + +message RuleChainMetadataRequestMsg { + int64 ruleChainIdMSB = 1; + int64 ruleChainIdLSB = 2; +} + +message AttributesRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; + string scope = 4; +} + +message RelationRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; +} + +message UserCredentialsRequestMsg { + int64 userIdMSB = 1; + int64 userIdLSB = 2; +} + +message DeviceCredentialsRequestMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; +} + +message DeviceRpcCallMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + int64 requestUuidMSB = 3; + int64 requestUuidLSB = 4; + int32 requestId = 5; + int64 expirationTime = 6; + bool oneway = 7; + RpcRequestMsg requestMsg = 8; + RpcResponseMsg responseMsg = 9; +} + +message RpcRequestMsg { + string method = 1; + string params = 2; +} + +message RpcResponseMsg { + string response = 1; + string error = 2; +} + +enum EdgeEntityType { + DEVICE = 0; + ASSET = 1; +} + +/** + * Main Messages; + */ + +message UplinkMsg { + int32 uplinkMsgId = 1; + repeated EntityDataProto entityData = 2; + repeated DeviceUpdateMsg deviceUpdateMsg = 3; + repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; + repeated AlarmUpdateMsg alarmUpdateMsg = 5; + repeated RelationUpdateMsg relationUpdateMsg = 6; + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; + repeated AttributesRequestMsg attributesRequestMsg = 8; + repeated RelationRequestMsg relationRequestMsg = 9; + repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11; + repeated DeviceRpcCallMsg deviceRpcCallMsg = 12; +} + +message UplinkResponseMsg { + bool success = 1; + string errorMsg = 2; +} + +message DownlinkResponseMsg { + bool success = 1; + string errorMsg = 2; +} + +message DownlinkMsg { + int32 downlinkMsgId = 1; + repeated EntityDataProto entityData = 2; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 3; + repeated DeviceUpdateMsg deviceUpdateMsg = 4; + repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 5; + repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 6; + repeated RuleChainUpdateMsg ruleChainUpdateMsg = 7; + repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 8; + repeated DashboardUpdateMsg dashboardUpdateMsg = 9; + repeated AssetUpdateMsg assetUpdateMsg = 10; + repeated EntityViewUpdateMsg entityViewUpdateMsg = 11; + repeated AlarmUpdateMsg alarmUpdateMsg = 12; + repeated UserUpdateMsg userUpdateMsg = 13; + repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 14; + repeated CustomerUpdateMsg customerUpdateMsg = 15; + repeated RelationUpdateMsg relationUpdateMsg = 16; + repeated WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = 17; + repeated WidgetTypeUpdateMsg widgetTypeUpdateMsg = 18; + repeated AdminSettingsUpdateMsg adminSettingsUpdateMsg = 19; + repeated DeviceRpcCallMsg deviceRpcCallMsg = 20; +} + diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 13d0a6e9eb..d9f05e3c84 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -82,8 +82,12 @@ public enum MsgType { DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG, + DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG, + DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG, + DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG, + SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG, DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG, @@ -101,6 +105,11 @@ public enum MsgType { /** * Message that is sent by TransportRuleEngineService to Device Actor. Represents messages from the device itself. */ - TRANSPORT_TO_DEVICE_ACTOR_MSG; + TRANSPORT_TO_DEVICE_ACTOR_MSG, + + /** + * Message that is sent on Edge Event to Edge Session + */ + EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java new file mode 100644 index 0000000000..544e60bba4 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.edge; + +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.aware.TenantAwareMsg; +import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; + +@ToString +public class EdgeEventUpdateMsg implements TenantAwareMsg, ToAllNodesMsg { + @Getter + private final TenantId tenantId; + @Getter + private final EdgeId edgeId; + + public EdgeEventUpdateMsg(TenantId tenantId, EdgeId edgeId) { + this.tenantId = tenantId; + this.edgeId = edgeId; + } + + @Override + public MsgType getMsgType() { + return MsgType.EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; + } +} diff --git a/common/pom.xml b/common/pom.xml index 7568d18ca6..892f4fb582 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -43,6 +43,8 @@ dao-api stats cache + coap-server + edge-api diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 1a17e438f1..b152e74d41 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -566,6 +566,21 @@ message FromDeviceRPCResponseProto { int32 error = 4; } +message EdgeNotificationMsgProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 edgeIdMSB = 3; + int64 edgeIdLSB = 4; + string type = 5; + string action = 6; + int64 entityIdMSB = 7; + int64 entityIdLSB = 8; + string entityType = 9; + string body = 10; + PostTelemetryMsg postTelemetryMsg = 11; + PostAttributeMsg postAttributesMsg = 12; +} + /** * Main messages; */ @@ -605,6 +620,7 @@ message ToCoreMsg { DeviceStateServiceMsgProto deviceStateServiceMsg = 2; SubscriptionMgrMsgProto toSubscriptionMgrMsg = 3; bytes toDeviceActorNotificationMsg = 4; + EdgeNotificationMsgProto edgeNotificationMsg = 5; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ @@ -612,6 +628,7 @@ message ToCoreNotificationMsg { LocalSubscriptionServiceMsgProto toLocalSubscriptionServiceMsg = 1; FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; bytes componentLifecycleMsg = 3; + bytes edgeEventUpdateMsg = 4; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 958f20337f..2fefb8f300 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -40,10 +40,18 @@ org.thingsboard.common.transport transport-api + + org.thingsboard.common + coap-server + org.eclipse.californium californium-core + + org.eclipse.californium + scandium + org.springframework spring-context-support diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 8cd117e99c..9133809225 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -18,13 +18,12 @@ package org.thingsboard.server.transport.coap; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; +import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; /** @@ -35,18 +34,6 @@ import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; @Component public class CoapTransportContext extends TransportContext { - @Getter - @Value("${transport.coap.bind_address}") - private String host; - - @Getter - @Value("${transport.coap.bind_port}") - private Integer port; - - @Getter - @Value("${transport.coap.timeout}") - private Long timeout; - @Getter @Autowired private JsonCoapAdaptor jsonCoapAdaptor; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index c1fd5b6a4a..8d756631e8 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -27,6 +27,9 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceObserver; +import org.springframework.util.StringUtils; +import org.thingsboard.server.coapserver.CoapServerService; +import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -63,15 +66,24 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private static final int FEATURE_TYPE_POSITION = 4; private static final int REQUEST_ID_POSITION = 5; + private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3; + private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4; + private static final String DTLS_SESSION_ID_KEY = "DTLS_SESSION_ID"; + private final ConcurrentMap tokenToSessionIdMap = new ConcurrentHashMap<>(); private final ConcurrentMap tokenToNotificationCounterMap = new ConcurrentHashMap<>(); private final Set rpcSubscriptions = ConcurrentHashMap.newKeySet(); private final Set attributeSubscriptions = ConcurrentHashMap.newKeySet(); - public CoapTransportResource(CoapTransportContext coapTransportContext, String name) { + private ConcurrentMap dtlsSessionIdMap; + private long timeout; + + public CoapTransportResource(CoapTransportContext coapTransportContext, CoapServerService coapServerService, String name) { super(coapTransportContext, name); this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); + this.dtlsSessionIdMap = coapServerService.getDtlsSessionsMap(); + this.timeout = coapServerService.getTimeout(); // this.setObservable(false); // disable observing // this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs // this.getAttributes().setObservable(); // mark observable in the Link-Format @@ -187,107 +199,132 @@ public class CoapTransportResource extends AbstractCoapTransportResource { Exchange advanced = exchange.advanced(); Request request = advanced.getRequest(); + String dtlsSessionIdStr = request.getSourceContext().get(DTLS_SESSION_ID_KEY); + if (!StringUtils.isEmpty(dtlsSessionIdStr)) { + if (dtlsSessionIdMap != null) { + TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionIdMap + .computeIfPresent(dtlsSessionIdStr, (dtlsSessionId, dtlsSessionInfo) -> { + dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis()); + return dtlsSessionInfo; + }); + if (tbCoapDtlsSessionInfo != null) { + processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getSessionInfoProto(), tbCoapDtlsSessionInfo.getDeviceProfile()); + } else { + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } + + private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) { Optional credentials = decodeCredentials(request); if (credentials.isEmpty()) { - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); return; } - transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> { - UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - try { - TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); - CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); - switch (type) { - case POST_ATTRIBUTES_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostAttributes(sessionId, request, - transportConfigurationContainer.getAttributesMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case POST_TELEMETRY_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostTelemetry(sessionId, request, - transportConfigurationContainer.getTelemetryMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case CLAIM_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case SUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentAttrSession == null) { - attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); - } - break; - case UNSUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (attrSession != null) { - UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); - attributeSubscriptions.remove(attrSessionId); - transportService.process(attrSession, - TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case SUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentRpcSession == null) { - rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), - new CoapNoOpCallback(exchange)); - } else { - UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); - reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); - } - break; - case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (rpcSession != null) { - UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); - rpcSubscriptions.remove(rpcSessionId); - transportService.process(rpcSession, - TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case TO_DEVICE_RPC_RESPONSE: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case TO_SERVER_RPC_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - case GET_ATTRIBUTES_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToGetAttributes(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - } - } catch (AdaptorException e) { - log.trace("[{}] Failed to decode message: ", sessionId, e); - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } + processRequest(exchange, type, request, sessionInfo, deviceProfile); })); } + private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { + UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); + try { + TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); + CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); + switch (type) { + case POST_ATTRIBUTES_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostAttributes(sessionId, request, + transportConfigurationContainer.getAttributesMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case POST_TELEMETRY_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostTelemetry(sessionId, request, + transportConfigurationContainer.getTelemetryMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case CLAIM_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case SUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentAttrSession == null) { + attributeSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); + } + break; + case UNSUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (attrSession != null) { + UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); + attributeSubscriptions.remove(attrSessionId); + transportService.process(attrSession, + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case SUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentRpcSession == null) { + rpcSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), + new CoapNoOpCallback(exchange)); + } else { + UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); + reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); + } + break; + case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (rpcSession != null) { + UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); + rpcSubscriptions.remove(rpcSessionId); + transportService.process(rpcSession, + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case TO_DEVICE_RPC_RESPONSE: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case TO_SERVER_RPC_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), timeout); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + case GET_ATTRIBUTES_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), timeout); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToGetAttributes(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + } + } catch (AdaptorException e) { + log.trace("[{}] Failed to decode message: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + } + private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) { tokenToNotificationCounterMap.remove(token); return tokenToSessionIdMap.remove(token); @@ -310,7 +347,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private Optional decodeCredentials(Request request) { List uriPath = request.getOptions().getUriPath(); - if (uriPath.size() >= ACCESS_TOKEN_POSITION) { + if (uriPath.size() > ACCESS_TOKEN_POSITION) { return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1))); } else { return Optional.empty(); @@ -322,8 +359,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= FEATURE_TYPE_POSITION) { return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); - } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) { - return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { + if (uriPath.contains(DataConstants.PROVISION)) { + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } + return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST - 1).toUpperCase())); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); @@ -336,6 +376,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= REQUEST_ID_POSITION) { return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1))); + } else { + return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1))); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index 4882180893..89b7b5b4bc 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -18,18 +18,16 @@ package org.thingsboard.server.transport.coap; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.core.server.resources.Resource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TbTransportService; +import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; @Service("CoapTransportService") @@ -42,50 +40,31 @@ public class CoapTransportService implements TbTransportService { private static final String EFENTO = "efento"; private static final String MEASUREMENTS = "m"; + @Autowired + private CoapServerService coapServerService; + @Autowired private CoapTransportContext coapTransportContext; - private CoapServer server; + private CoapServer coapServer; @PostConstruct public void init() throws UnknownHostException { log.info("Starting CoAP transport..."); - log.info("Starting CoAP transport server"); - - this.server = new CoapServer(); - createResources(); - Resource root = this.server.getRoot(); - TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); - this.server.setMessageDeliverer(messageDeliverer); - - InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); - InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); - - CoapEndpoint.Builder coapEndpoitBuilder = new CoapEndpoint.Builder(); - coapEndpoitBuilder.setInetSocketAddress(sockAddr); - CoapEndpoint coapEndpoint = coapEndpoitBuilder.build(); - - server.addEndpoint(coapEndpoint); - server.start(); - log.info("CoAP transport started!"); - } - - private void createResources() { + coapServer = coapServerService.getCoapServer(); CoapResource api = new CoapResource(API); - api.add(new CoapTransportResource(coapTransportContext, V1)); + api.add(new CoapTransportResource(coapTransportContext, coapServerService, V1)); CoapResource efento = new CoapResource(EFENTO); CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS); efento.add(efentoMeasurementsTransportResource); - - server.add(api); - server.add(efento); + coapServer.add(api); + coapServer.add(efento); + log.info("CoAP transport started!"); } @PreDestroy public void shutdown() { - log.info("Stopping CoAP transport!"); - this.server.destroy(); log.info("CoAP transport stopped!"); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java new file mode 100644 index 0000000000..f9a31d0513 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2021 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class NoSecClient { + + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public NoSecClient(String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + this.coapClient = new CoapClient(uri); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecClient " + + "host port accessToken clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[3]; + String sharedKeys = args[4]; + + NoSecClient client = new NoSecClient(host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java new file mode 100644 index 0000000000..7bbb1f55cf --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java @@ -0,0 +1,145 @@ +/** + * Copyright © 2016-2021 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientNoAuth { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientNoAuth(DTLSConnector dtlsConnector, String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, accessToken, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientNoAuth " + + "host port accessToken keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[7]; + String sharedKeys = args[8]; + + String keyStoreUriPath = args[3]; + String keyStoreAlias = args[4]; + String trustedAliasPattern = args[5]; + String keyStorePassword = args[6]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientNoAuth client = new SecureClientNoAuth(dtlsConnector, host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java new file mode 100644 index 0000000000..31dd628b40 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2021 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientX509 { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientX509(DTLSConnector dtlsConnector, String host, int port, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientX509 " + + "host port keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String clientKeys = args[6]; + String sharedKeys = args[7]; + + String keyStoreUriPath = args[2]; + String keyStoreAlias = args[3]; + String trustedAliasPattern = args[4]; + String keyStorePassword = args[5]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientX509 client = new SecureClientX509(dtlsConnector, host, port, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 15fb015f32..20baeba453 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -20,10 +20,11 @@ import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; @@ -85,4 +86,16 @@ public class LwM2mSessionMsgListener implements GenericFutureListener future) throws Exception { log.info("[{}] operationComplete", future); } + + public void onResourceUpdate(Optional resourceUpdateMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.get().getResourceType())) { + this.service.onResourceUpdate(resourceUpdateMsgOpt); + } + } + + public void onResourceDelete(Optional resourceDeleteMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceDeleteMsgOpt.get().getResourceType())) { + this.service.onResourceDelete(resourceDeleteMsgOpt); + } + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java index cae3eeb05d..a879e4bc78 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -83,7 +83,7 @@ public class LwM2mTransportRequest { private LwM2mValueConverterImpl converter; - private final LwM2mTransportContextServer context; + private final LwM2mTransportContextServer lwM2mTransportContextServer; private final LwM2mClientContext lwM2mClientContext; @@ -91,8 +91,8 @@ public class LwM2mTransportRequest { private final LwM2mTransportServiceImpl serviceImpl; - public LwM2mTransportRequest(LwM2mTransportContextServer context, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer, LwM2mTransportServiceImpl serviceImpl) { - this.context = context; + public LwM2mTransportRequest(LwM2mTransportContextServer lwM2mTransportContextServer, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer, LwM2mTransportServiceImpl serviceImpl) { + this.lwM2mTransportContextServer = lwM2mTransportContextServer; this.lwM2mClientContext = lwM2mClientContext; this.leshanServer = leshanServer; this.serviceImpl = serviceImpl; @@ -101,7 +101,7 @@ public class LwM2mTransportRequest { @PostConstruct public void init() { this.converter = LwM2mValueConverterImpl.getInstance(); - executorResponse = Executors.newFixedThreadPool(this.context.getLwM2MTransportConfigServer().getRequestPoolSize(), + executorResponse = Executors.newFixedThreadPool(this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResponsePoolSize(), new NamedThreadFactory(String.format("LwM2M %s channel response", RESPONSE_CHANNEL))); } @@ -109,18 +109,20 @@ public class LwM2mTransportRequest { * Device management and service enablement, including Read, Write, Execute, Discover, Create, Delete and Write-Attributes * * @param registration - - * @param target - + * @param targetIdVer - * @param typeOper - * @param contentFormatParam - * @param observation - */ - public void sendAllRequest(Registration registration, String target, String typeOper, + public void sendAllRequest(Registration registration, String targetIdVer, String typeOper, String contentFormatParam, Observation observation, Object params, long timeoutInMs) { - LwM2mPath resultIds = new LwM2mPath(convertToObjectIdFromIdVer(target)); + String target = convertToObjectIdFromIdVer(targetIdVer); + LwM2mPath resultIds = new LwM2mPath(target); if (registration != null && resultIds.getObjectId() >= 0) { DownlinkRequest request = null; ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null; - ResourceModel resource = serviceImpl.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(registration, resultIds); + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + ResourceModel resource = lwM2MClient.getResourceModel(targetIdVer); timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; switch (typeOper) { case GET_TYPE_OPER_READ: @@ -217,7 +219,10 @@ public class LwM2mTransportRequest { } if (request != null) { - this.sendRequest(registration, request, timeoutInMs); + this.sendRequest(registration, lwM2MClient, request, timeoutInMs); + } + else { + log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); } } } @@ -230,8 +235,7 @@ public class LwM2mTransportRequest { */ @SuppressWarnings("unchecked") - private void sendRequest(Registration registration, DownlinkRequest request, long timeoutInMs) { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + private void sendRequest(Registration registration, LwM2mClient lwM2MClient, DownlinkRequest request, long timeoutInMs) { leshanServer.send(registration, request, timeoutInMs, (ResponseCallback) response -> { if (!lwM2MClient.isInit()) { lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java index db419eca9f..982e7cd74c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java @@ -48,6 +48,10 @@ public interface LwM2mTransportService extends TbTransportService { void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); + void onResourceUpdate (Optional resourceUpdateMsgOpt); + + void onResourceDelete(Optional resourceDeleteMsgOpt); + void doTrigger(Registration registration, String path); void doDisconnect(TransportProtos.SessionInfoProto sessionInfo); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java index 3943bc2a51..773fe2b729 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java @@ -53,7 +53,6 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; 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.client.LwM2mClientProfile; -import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; @@ -74,9 +73,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import static org.thingsboard.server.common.transport.util.JsonUtils.getJsonObject; @@ -108,8 +104,6 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private ExecutorService executorUpdateRegistered; private ExecutorService executorUnRegistered; private LwM2mValueConverterImpl converter; - protected final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - protected final Lock writeLock = readWriteLock.writeLock(); private final TransportService transportService; @@ -163,7 +157,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { if (lwM2MClient != null) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); if (sessionInfo != null) { - this.initLwM2mClient (lwM2MClient, sessionInfo); + this.initLwM2mClient(lwM2MClient, sessionInfo); transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), null); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); @@ -224,7 +218,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { }); } - private void initLwM2mClient (LwM2mClient lwM2MClient, SessionInfoProto sessionInfo) { + private void initLwM2mClient(LwM2mClient lwM2MClient, SessionInfoProto sessionInfo) { lwM2MClient.setDeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); lwM2MClient.setProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); lwM2MClient.setDeviceName(sessionInfo.getDeviceName()); @@ -310,7 +304,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); LwM2mClientProfile clientProfile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); if (pathIdVer != null && !pathIdVer.isEmpty() && (this.validatePathInAttrProfile(clientProfile, pathIdVer) || this.validatePathInTelemetryProfile(clientProfile, pathIdVer))) { - ResourceModel resourceModel = lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(convertToObjectIdFromIdVer(pathIdVer))); + ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer); if (resourceModel != null && resourceModel.operations.isWritable()) { lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), pathIdVer, POST_TYPE_OPER_WRITE_REPLACE, ContentFormat.TLV.getName(), null, value, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); @@ -361,6 +355,26 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { registrationIdOpt.ifPresent(registrationId -> this.onDeviceUpdateLwM2MClient(registrationId, device, deviceProfileOpt)); } + /** + * + * @param resourceUpdateMsgOpt - + */ + @Override + public void onResourceUpdate (Optional resourceUpdateMsgOpt) { + String idVer = resourceUpdateMsgOpt.get().getResourceKey(); + lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.updateResourceModel(idVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); + } + + /** + * + * @param resourceDeleteMsgOpt - + */ + @Override + public void onResourceDelete(Optional resourceDeleteMsgOpt) { + String pathIdVer = resourceDeleteMsgOpt.get().getResourceKey(); + lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.deleteResources(pathIdVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); + } + /** * Trigger Server path = "/1/0/8" *

@@ -520,10 +534,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private void updateResourcesValue(Registration registration, LwM2mResource lwM2mResource, String path) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); - lwM2MClient.updateResourceValue(path, lwM2mResource); - Set paths = new HashSet<>(); - paths.add(path); - this.updateAttrTelemetry(registration, paths); + if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())) { + Set paths = new HashSet<>(); + paths.add(path); + this.updateAttrTelemetry(registration, paths); + } else { + log.error("Fail update Resource [{}]", lwM2mResource); + } } /** @@ -539,12 +556,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { JsonObject attributes = new JsonObject(); JsonObject telemetries = new JsonObject(); try { - writeLock.lock(); this.getParametersFromProfile(attributes, telemetries, registration, paths); } catch (Exception e) { log.error("UpdateAttrTelemetry", e); - } finally { - writeLock.unlock(); } if (attributes.getAsJsonObject().entrySet().size() > 0) this.updateParametersOnThingsboard(attributes, DEVICE_ATTRIBUTES_TOPIC, registration); @@ -559,8 +573,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private boolean validatePathInAttrProfile(LwM2mClientProfile clientProfile, String path) { try { - List attributesSet = new Gson().fromJson(clientProfile.getPostAttributeProfile(), new TypeToken>() { - }.getType()); + List attributesSet = new Gson().fromJson(clientProfile.getPostAttributeProfile(), + new TypeToken>() { + }.getType()); return attributesSet.stream().anyMatch(p -> p.equals(path)); } catch (Exception e) { log.error("Fail Validate Path [{}] ClientProfile.Attribute", path, e); @@ -699,7 +714,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private void addParameters(String path, JsonObject parameters, Registration registration) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); JsonObject names = lwM2mClientContext.getProfiles().get(lwM2MClient.getProfileId()).getPostKeyNameProfile(); - String resName = String.valueOf(names.get(path)); + String resName = names.get(path).getAsString(); if (resName != null && !resName.isEmpty()) { try { String resValue = this.getResourceValueToString(lwM2MClient, path); @@ -718,9 +733,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private String getResourceValueToString(LwM2mClient lwM2MClient, String path) { LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(path)); - ResourceValue resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, path); + LwM2mResource resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, path); return resourceValue == null ? null : - this.converter.convertValue(resourceValue.getResourceValue(), this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModelType(lwM2MClient.getRegistration(), pathIds), ResourceModel.Type.STRING, pathIds).toString(); + this.converter.convertValue(resourceValue.isMultiInstances() ? resourceValue.getValues() : resourceValue.getValue(), resourceValue.getType(), ResourceModel.Type.STRING, pathIds).toString(); } /** @@ -728,10 +743,10 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param path - * @return - return value of Resource by idPath */ - private ResourceValue returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { - ResourceValue resourceValue = null; + private LwM2mResource returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { + LwM2mResource resourceValue = null; if (new LwM2mPath(convertToObjectIdFromIdVer(path)).isResource()) { - resourceValue = lwM2MClient.getResources().get(path); + resourceValue = lwM2MClient.getResources().get(path).getLwM2mResource(); } return resourceValue; } @@ -816,7 +831,6 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { if (sendAttrToThingsboard.getPathPostParametersAdd().size() > 0) { // update value in Resources registrationIds.forEach(registrationId -> { -// LeshanServer lwServer = leshanServer; Registration registration = lwM2mClientContext.getRegistration(registrationId); this.readResourceValueObserve(registration, sendAttrToThingsboard.getPathPostParametersAdd(), GET_TYPE_OPER_READ); // send attr/telemetry to tingsboard for new path @@ -968,9 +982,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private String getPathAttributeUpdateProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { LwM2mClientProfile profile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); - Registration registration = lwM2mClientContext.getLwM2MClient(sessionInfo).getRegistration(); + LwM2mClient lwM2mClient = lwM2mClientContext.getLwM2MClient(sessionInfo); return profile.getPostKeyNameProfile().getAsJsonObject().entrySet().stream() - .filter(e -> e.getValue().getAsString().equals(name) && validateResourceInModel(registration, e.getKey(), false)).findFirst().map(Map.Entry::getKey) + .filter(e -> e.getValue().getAsString().equals(name) && validateResourceInModel(lwM2mClient, e.getKey(), false)).findFirst().map(Map.Entry::getKey) .orElse(""); } @@ -1119,7 +1133,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { ConcurrentMap keyNamesIsWritable = keyNamesMap.entrySet() .stream() - .filter(e -> (attrSet.contains(e.getKey()) && validateResourceInModel(lwM2MClient.getRegistration(), e.getKey(), true))) + .filter(e -> (attrSet.contains(e.getKey()) && validateResourceInModel(lwM2MClient, e.getKey(), true))) .collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue)); Set namesIsWritable = ConcurrentHashMap.newKeySet(); @@ -1127,18 +1141,18 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { return new ArrayList<>(namesIsWritable); } + private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathKey, boolean isWritable) { + ResourceModel resourceModel = lwM2mClient.getResourceModel(pathKey); + Integer objectId = validateObjectIdFromKey(pathKey); + String objectVer = validateObjectVerFromKey(pathKey); + return resourceModel != null && (isWritable ? + objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)) && resourceModel.operations.isWritable() : + objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId))); + } + @Override public String getName() { return "LWM2M"; } - private boolean validateResourceInModel(Registration registration, String pathKey, boolean isWritable) { - ResourceModel resourceModel = lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(registration, - new LwM2mPath(convertToObjectIdFromIdVer(pathKey))); - Integer objectId = validateObjectIdFromKey(pathKey); - String objectVer = validateObjectVerFromKey(pathKey); - return resourceModel != null && (isWritable ? - objectId != null && objectVer != null && objectVer.equals(registration.getSupportedVersion(objectId)) && resourceModel.operations.isWritable() : - objectId != null && objectVer != null && objectVer.equals(registration.getSupportedVersion(objectId))); - } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index fc35668fef..3705547325 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -85,7 +85,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { if (objectModel != null) return objectModel.resources.get(resourceId); else - log.warn("TbResources (Object model) with id [{}/{}] not found on the server", objectId, resourceId); + log.warn("TbResources (Object model) with id [{}/0/{}] not found on the server", objectId, resourceId); return null; } catch (Exception e) { log.error("", e); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 1c9afaab6d..bdb783e3ff 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -17,9 +17,10 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; -import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -28,9 +29,14 @@ import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; 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.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; @Slf4j @Data @@ -67,21 +73,79 @@ public class LwM2mClient implements Cloneable { this.init = false; } - public void updateResourceValue(String pathRez, LwM2mResource rez) { - if (rez instanceof LwM2mMultipleResource) { - this.resources.put(pathRez, new ResourceValue(rez.getValues(), null, true)); - } else if (rez instanceof LwM2mSingleResource) { - this.resources.put(pathRez, new ResourceValue(null, rez.getValue(), false)); + public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { + if (this.resources.get(pathRez) != null && this.resources.get(pathRez).getResourceModel() != null) { + this.resources.get(pathRez).setLwM2mResource(rez); + return true; + } else { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + if (resourceModel != null) { + this.resources.put(pathRez, new ResourceValue(rez, resourceModel)); + return true; + } else { + return false; + } + } + } + + public ResourceModel getResourceModel(String pathRez) { + if (this.getResources().get(pathRez) != null) { + return this.getResources().get(pathRez).getResourceModel(); + } else { + return null; } } - public void initValue(LwM2mTransportServiceImpl lwM2MTransportService, String path) { + /** + * + * @param pathIdVer == "3_1.0" + * @param modelProvider - + */ + public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { + Set key = getKeysEqualsIdVer(pathIdVer); + key.forEach(pathRez -> { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez.toString())); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + if (resourceModel != null) { + this.resources.get(pathRez).setResourceModel(resourceModel); + } + else { + this.resources.remove(pathRez); + } + }); + } + + /** + * + * @param idVer - + * @param modelProvider - + */ + public void updateResourceModel(String idVer, LwM2mModelProvider modelProvider) { + Set key = getKeysEqualsIdVer(idVer); + key.forEach(k -> this.saveResourceModel(k.toString(), modelProvider)); + } + + private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + this.resources.get(pathRez).setResourceModel(resourceModel); + } + + private Set getKeysEqualsIdVer(String idVer) { + return this.resources.keySet() + .stream() + .filter(e -> idVer.equals(e.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toSet()); + } + + public void initValue(LwM2mTransportServiceImpl serviceImpl, String path) { if (path != null) { this.pendingRequests.remove(path); } if (this.pendingRequests.size() == 0) { this.init = true; - lwM2MTransportService.putDelayedUpdateResourcesThingsboard(this); + serviceImpl.putDelayedUpdateResourcesThingsboard(this); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java index 8285c9bc8b..1c4042bd1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java @@ -34,25 +34,25 @@ public class LwM2mClientProfile { /** * {"keyName": { - * "/3/0/1": "modelNumber", - * "/3/0/0": "manufacturer", - * "/3/0/2": "serialNumber" + * "/3_1.0/0/1": "modelNumber", + * "/3_1.0/0/0": "manufacturer", + * "/3_1.0/0/2": "serialNumber" * } **/ private JsonObject postKeyNameProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0/0", "/3_1.0/0/1"] */ private JsonArray postAttributeProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0/0", "/3_1.0/0/2"] */ private JsonArray postTelemetryProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0", "/3_1.0/0/1, "/3_1.0/0/2"] */ private JsonArray postObserveProfile; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java index 3ff04f288b..cbaf60ca77 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java @@ -16,23 +16,16 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Data; - -import java.util.Map; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mResource; @Data public class ResourceValue { - Map values; - Object value; - boolean multiInstances; - - public ResourceValue(Map values, Object value, boolean multiInstances) { - this.values = values; - this.value = value; - this.multiInstances = multiInstances; - } + private LwM2mResource lwM2mResource; + private ResourceModel resourceModel; - public Object getResourceValue() { - return this.multiInstances ? this.values : this.value; + public ResourceValue(LwM2mResource lwM2mResource, ResourceModel resourceModel) { + this.lwM2mResource = lwM2mResource; + this.resourceModel = resourceModel; } - } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index c2cf3686e9..64efb5f036 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.mqtt.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -41,7 +41,6 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyStore; @@ -56,7 +55,7 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Component("MqttSslHandlerProvider") -@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${transport.mqtt.enabled}'=='true'") @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) public class MqttSslHandlerProvider { 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 06a8dcacdc..6056aa293d 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 @@ -66,7 +66,7 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; 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.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; import java.security.cert.Certificate; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index a004fbd500..c948e755c2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -146,7 +146,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), rpcRequest.toByteArray())); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest))); } @Override diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 9aff642708..434441bdee 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -19,11 +19,11 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import java.util.Optional; @@ -45,12 +45,14 @@ public interface SessionMsgListener { default void onToTransportUpdateCredentials(ToTransportUpdateCredentialsProto toTransportUpdateCredentials){} - default void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) { - } + default void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) {} + + default void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, + Optional deviceProfileOpt) {} + + default void onDeviceDeleted(DeviceId deviceId) {} - default void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { - } + default void onResourceUpdate(Optional resourceUpdateMsgOpt) {} - default void onDeviceDeleted(DeviceId deviceId) { - } + default void onResourceDelete(Optional resourceUpdateMsgOpt) {} } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 699e43e5ee..fe2f3500a6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -74,12 +74,16 @@ public class JsonConverter { private static int maxStringValueLength = 0; - public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) throws JsonSyntaxException { + public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement, long ts) throws JsonSyntaxException { PostTelemetryMsg.Builder builder = PostTelemetryMsg.newBuilder(); - convertToTelemetry(jsonElement, System.currentTimeMillis(), null, builder); + convertToTelemetry(jsonElement, ts, null, builder); return builder.build(); } + public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) throws JsonSyntaxException { + return convertToTelemetryProto(jsonElement, System.currentTimeMillis()); + } + private static void convertToTelemetry(JsonElement jsonElement, long systemTs, Map> result, PostTelemetryMsg.Builder builder) { if (jsonElement.isJsonObject()) { parseObject(systemTs, result, builder, jsonElement.getAsJsonObject()); @@ -107,7 +111,7 @@ public class JsonConverter { public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) { long durationMs = 0L; if (json != null && !json.isEmpty()) { - return convertToClaimDeviceProto(deviceId, new JsonParser().parse(json)); + return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json)); } return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs); } @@ -156,7 +160,7 @@ public class JsonConverter { result.addProperty("id", msg.getRequestId()); } result.addProperty("method", msg.getMethodName()); - result.add("params", new JsonParser().parse(msg.getParams())); + result.add("params", JSON_PARSER.parse(msg.getParams())); return result; } @@ -405,7 +409,7 @@ public class JsonConverter { public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) { if (StringUtils.isEmpty(msg.getError())) { - return new JsonParser().parse(msg.getPayload()); + return JSON_PARSER.parse(msg.getPayload()); } else { JsonObject errorMsg = new JsonObject(); errorMsg.addProperty("error", msg.getError()); @@ -563,7 +567,7 @@ public class JsonConverter { } public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { - JsonElement jsonElement = new JsonParser().parse(json); + JsonElement jsonElement = JSON_PARSER.parse(json); if (jsonElement.isJsonObject()) { return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); } else { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index fc82f78633..ed440ebcfe 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.common.transport.adaptor; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; @@ -167,4 +169,27 @@ public class ProtoConverter { }); return kvList; } + + public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { + TransportProtos.ToDeviceRpcRequestMsg.Builder toDeviceRpcRequestMsgBuilder = toDeviceRpcRequestMsg.newBuilderForType(); + toDeviceRpcRequestMsgBuilder.mergeFrom(toDeviceRpcRequestMsg); + toDeviceRpcRequestMsgBuilder.setParams(parseParams(toDeviceRpcRequestMsg)); + TransportProtos.ToDeviceRpcRequestMsg result = toDeviceRpcRequestMsgBuilder.build(); + return result.toByteArray(); + } + + private static String parseParams(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { + String params = toDeviceRpcRequestMsg.getParams(); + JsonElement jsonElementParams = JSON_PARSER.parse(params); + if (!jsonElementParams.isJsonPrimitive()) { + return params; + } else { + JsonPrimitive primitiveParams = jsonElementParams.getAsJsonPrimitive(); + if (jsonElementParams.getAsJsonPrimitive().isString()) { + return primitiveParams.getAsString(); + } else { + return params; + } + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java index ab18b930f9..b175ca8580 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -25,7 +25,15 @@ import java.util.UUID; public class SessionInfoCreator { public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { - return TransportProtos.SessionInfoProto.newBuilder().setNodeId(context.getNodeId()) + return getSessionInfoProto(msg, context.getNodeId(), sessionId); + } + + public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return getSessionInfoProto(msg, nodeId, sessionId); + } + + private static TransportProtos.SessionInfoProto getSessionInfoProto(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return TransportProtos.SessionInfoProto.newBuilder().setNodeId(nodeId) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java index ebd439b12b..5603721474 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java @@ -18,10 +18,7 @@ package org.thingsboard.server.common.transport.lwm2m; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.ResourceModel; -import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.model.LwM2mModelProvider; -import org.eclipse.leshan.server.registration.Registration; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; @@ -95,12 +92,8 @@ public class LwM2MTransportConfigServer { private boolean recommendedSupportedGroups; @Getter - @Value("${transport.lwm2m.request_pool_size:}") - private int requestPoolSize; - - @Getter - @Value("${transport.lwm2m.request_error_pool_size:}") - private int requestErrorPoolSize; + @Value("${transport.lwm2m.response_pool_size:}") + private int responsePoolSize; @Getter @Value("${transport.lwm2m.registered_pool_size:}") @@ -219,13 +212,4 @@ public class LwM2MTransportConfigServer { } return FULL_FILE_PATH.toUri().getPath(); } - - public ResourceModel getResourceModel(Registration registration, LwM2mPath pathIds) { - return this.modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); - } - - public ResourceModel.Type getResourceModelType(Registration registration, LwM2mPath pathIds) { - ResourceModel resource = this.getResourceModel(registration, pathIds); - return (resource == null) ? null : resource.type; - } } 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 95a1bf82a4..374e9528e6 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 @@ -345,7 +345,7 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback) { + public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateDeviceLwM2MCredentialsRequestMsg(msg).build()); AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), @@ -771,12 +771,21 @@ public class DefaultTransportService implements TransportService { ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); String resourceId = msg.getResourceKey(); transportResourceCache.update(tenantId, resourceType, resourceId); + sessions.forEach((id, mdRez) -> { + log.warn("ResourceUpdate - [{}] [{}]", id, mdRez); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(Optional.ofNullable(msg))); + }); + } else if (toSessionMsg.hasResourceDeleteMsg()) { TransportProtos.ResourceDeleteMsg msg = toSessionMsg.getResourceDeleteMsg(); TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); String resourceId = msg.getResourceKey(); transportResourceCache.evict(tenantId, resourceType, resourceId); + sessions.forEach((id, mdRez) -> { + log.warn("ResourceDelete - [{}] [{}]", id, mdRez); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(Optional.ofNullable(msg))); + }); } else { //TODO: should we notify the device actor about missed session? log.debug("[{}] Missing session.", sessionId); @@ -899,7 +908,7 @@ public class DefaultTransportService implements TransportService { } private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, - TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { + TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java similarity index 93% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index f376077b84..77e4045655 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.mqtt.util; +package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; -import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; diff --git a/dao/pom.xml b/dao/pom.xml index 5c8d0d9ef5..9ba3033970 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -111,6 +111,14 @@ com.fasterxml.jackson.core jackson-databind + + org.hibernate.validator + hibernate-validator + + + org.glassfish + javax.el + org.springframework spring-context @@ -198,6 +206,11 @@ hsqldb test + + org.junit.jupiter + junit-jupiter-params + test + org.springframework spring-context-support 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 9af4dc4749..9dff668f2e 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 @@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -327,16 +326,6 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } - private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) { - log.debug("Deleting Alarm relation: {}", alarmRelation); - relationService.deleteRelation(tenantId, alarmRelation); - } - - private void createRelation(TenantId tenantId, EntityRelation alarmRelation) { - log.debug("Creating Alarm relation: {}", alarmRelation); - relationService.saveRelation(tenantId, alarmRelation); - } - private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index e99be2646d..130d86124f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; @@ -167,4 +168,24 @@ public interface AssetDao extends Dao, TenantEntityDao { */ ListenableFuture> findTenantAssetTypesAsync(UUID tenantId); + /** + * Find assets by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of asset objects + */ + PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find assets by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of asset objects + */ + PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 57a928ee0c..97bd5f37a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.asset; +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; @@ -37,24 +38,28 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -75,7 +80,6 @@ import static org.thingsboard.server.dao.service.Validator.validateString; public class BaseAssetService extends AbstractEntityService implements AssetService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_ASSET_ID = "Incorrect assetId "; public static final String TB_SERVICE_QUEUE = "TbServiceQueue"; @@ -89,9 +93,6 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private CustomerDao customerDao; - @Autowired - private EntityViewService entityViewService; - @Autowired private CacheManager cacheManager; @@ -324,6 +325,63 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ }, MoreExecutors.directExecutor()); } + @Override + public Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId) { + Asset asset = findAssetById(tenantId, assetId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign asset to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(asset.getTenantId().getId())) { + throw new DataValidationException("Can't assign asset to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, assetId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create asset relation. Edge Id: [{}]", assetId, edgeId); + throw new RuntimeException(e); + } + return asset; + } + + @Override + public Asset unassignAssetFromEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId) { + Asset asset = findAssetById(tenantId, assetId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign asset from non-existent edge!"); + } + + checkAssignedEntityViewsToEdge(tenantId, assetId, edgeId); + + try { + deleteRelation(tenantId, new EntityRelation(edgeId, assetId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete asset relation. Edge Id: [{}]", assetId, edgeId); + throw new RuntimeException(e); + } + return asset; + } + + @Override + public PageData findAssetsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findAssetsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findAssetsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findAssetsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + private DataValidator assetValidator = new DataValidator() { 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 7bf13871c5..2fdee60313 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 @@ -295,6 +295,22 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.put("startTs", extractParameter(Long.class, 1, additionalInfo)); actionData.put("endTs", extractParameter(Long.class, 2, additionalInfo)); break; + case ASSIGNED_TO_EDGE: + strEntityId = extractParameter(String.class, 0, additionalInfo); + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + actionData.put("entityId", strEntityId); + actionData.put("assignedEdgeId", strEdgeId); + actionData.put("assignedEdgeName", strEdgeName); + break; + case UNASSIGNED_FROM_EDGE: + strEntityId = extractParameter(String.class, 0, additionalInfo); + strEdgeId = extractParameter(String.class, 1, additionalInfo); + strEdgeName = extractParameter(String.class, 2, additionalInfo); + actionData.put("entityId", strEntityId); + actionData.put("unassignedEdgeId", strEdgeId); + actionData.put("unassignedEdgeName", strEdgeName); + break; } return actionData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index dc1446ff3a..c00c5fbe56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -106,16 +106,16 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic @Override protected void validateDataImpl(TenantId tenantId, ComponentDescriptor plugin) { if (plugin.getType() == null) { - throw new DataValidationException("Component type should be specified!."); + throw new DataValidationException("Component type should be specified!"); } if (plugin.getScope() == null) { - throw new DataValidationException("Component scope should be specified!."); + throw new DataValidationException("Component scope should be specified!"); } if (StringUtils.isEmpty(plugin.getName())) { - throw new DataValidationException("Component name should be specified!."); + throw new DataValidationException("Component name should be specified!"); } if (StringUtils.isEmpty(plugin.getClazz())) { - throw new DataValidationException("Component clazz should be specified!."); + throw new DataValidationException("Component clazz should be specified!"); } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 2ca59b7804..8f13900a6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -125,6 +125,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom entityViewService.unassignCustomerEntityViews(customer.getTenantId(), customerId); assetService.unassignCustomerAssets(customer.getTenantId(), customerId); deviceService.unassignCustomerDevices(customer.getTenantId(), customerId); + edgeService.unassignCustomerEdges(customer.getTenantId(), customerId); userService.deleteCustomerUsers(customer.getTenantId(), customerId); deleteEntityRelations(tenantId, customerId); customerDao.removeById(tenantId, customerId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 48b234f1b2..22a4567198 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -46,4 +46,14 @@ public interface DashboardInfoDao extends Dao { */ PageData findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + /** + * Find dashboards by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of dashboard objects + */ + PageData findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 84b2afc089..456132a443 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -26,15 +26,19 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -43,8 +47,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; -import java.util.concurrent.ExecutionException; - import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -64,6 +66,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private CustomerDao customerDao; + + @Autowired + private EdgeDao edgeDao; @Autowired @Lazy @@ -117,7 +122,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (dashboard.addAssignedCustomer(customer)) { try { createRelation(tenantId, new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create dashboard relation. Customer Id: [{}]", dashboardId, customerId); throw new RuntimeException(e); } @@ -137,7 +142,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (dashboard.removeAssignedCustomer(customer)) { try { deleteRelation(tenantId, new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to delete dashboard relation. Customer Id: [{}]", dashboardId, customerId); throw new RuntimeException(e); } @@ -156,16 +161,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } - private void deleteRelation(TenantId tenantId, EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { - log.debug("Deleting Dashboard relation: {}", dashboardRelation); - relationService.deleteRelationAsync(tenantId, dashboardRelation).get(); - } - - private void createRelation(TenantId tenantId, EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { - log.debug("Creating Dashboard relation: {}", dashboardRelation); - relationService.saveRelationAsync(tenantId, dashboardRelation).get(); - } - @Override public void deleteDashboard(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing deleteDashboard [{}]", dashboardId); @@ -220,6 +215,50 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb new CustomerDashboardsUpdater(customer).removeEntities(tenantId, customer); } + @Override + public Dashboard assignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId) { + Dashboard dashboard = findDashboardById(tenantId, dashboardId); + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + if (edge == null) { + throw new DataValidationException("Can't assign dashboard to non-existent edge!"); + } + if (!edge.getTenantId().equals(dashboard.getTenantId())) { + throw new DataValidationException("Can't assign dashboard to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create dashboard relation. Edge Id: [{}]", dashboardId, edgeId); + throw new RuntimeException(e); + } + return dashboard; + } + + @Override + public Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId) { + Dashboard dashboard = findDashboardById(tenantId, dashboardId); + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + if (edge == null) { + throw new DataValidationException("Can't unassign dashboard from non-existent edge!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete dashboard relation. Edge Id: [{}]", dashboardId, edgeId); + throw new RuntimeException(e); + } + return dashboard; + } + + @Override + public PageData findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findDashboardsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + Validator.validatePageLink(pageLink); + return dashboardInfoDao.findDashboardsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + private DataValidator dashboardValidator = new DataValidator() { @Override @@ -259,9 +298,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb deleteDashboard(tenantId, new DashboardId(entity.getUuidId())); } }; - + private class CustomerDashboardsUnassigner extends PaginatedRemover { - + private Customer customer; CustomerDashboardsUnassigner(Customer customer) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index 547725b635..a6da6dc321 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; @@ -219,4 +220,24 @@ public interface DeviceDao extends Dao, TenantEntityDao { List findDevicesIdsByDeviceProfileTransportType(DeviceTransportType transportType); + /** + * Find devices by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find devices by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index dfa98d8a6c..80fcf04430 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfiguration; @@ -51,15 +52,19 @@ import org.thingsboard.server.common.data.device.data.DeviceTransportConfigurati import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; 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; @@ -68,7 +73,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -104,6 +108,8 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + @Autowired private DeviceDao deviceDao; @@ -119,9 +125,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceProfileService deviceProfileService; - @Autowired - private EntityViewService entityViewService; - @Autowired private CacheManager cacheManager; @@ -567,6 +570,63 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesIdsByDeviceProfileTransportType(transportType); } + @Override + public Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { + Device device = findDeviceById(tenantId, deviceId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign device to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(device.getTenantId().getId())) { + throw new DataValidationException("Can't assign device to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create device relation. Edge Id: [{}]", deviceId, edgeId); + throw new RuntimeException(e); + } + return device; + } + + @Override + public Device unassignDeviceFromEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { + Device device = findDeviceById(tenantId, deviceId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign device from non-existent edge!"); + } + + checkAssignedEntityViewsToEdge(tenantId, deviceId, edgeId); + + try { + deleteRelation(tenantId, new EntityRelation(edgeId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete device relation. Edge Id: [{}]", deviceId, edgeId); + throw new RuntimeException(e); + } + return device; + } + + @Override + public PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findDevicesByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findDevicesByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + private DataValidator deviceValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java new file mode 100644 index 0000000000..20e4fbf6ce --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Service +@Slf4j +public class BaseEdgeEventService implements EdgeEventService { + + @Autowired + private EdgeEventDao edgeEventDao; + + @Override + public ListenableFuture saveAsync(EdgeEvent edgeEvent) { + edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId); + return edgeEventDao.saveAsync(edgeEvent); + } + + @Override + public PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { + return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate); + } + + private DataValidator edgeEventValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, EdgeEvent edgeEvent) { + if (edgeEvent.getEdgeId() == null) { + throw new DataValidationException("Edge id should be specified!"); + } + if (edgeEvent.getAction() == null) { + throw new DataValidationException("Edge Event action should be specified!"); + } + } + }; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java new file mode 100644 index 0000000000..d254c5ee9a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * The Interface EdgeDao. + * + */ +public interface EdgeDao extends Dao { + + /** + * Save or update edge object + * + * @param edge the edge object + * @return saved edge object + */ + Edge save(TenantId tenantId, Edge edge); + + /** + * Find edge info by id. + * + * @param tenantId the tenant id + * @param edgeId the edge id + * @return the edge info object + */ + EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId); + + /** + * Find edges by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find edges by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find edges by tenantId and edges Ids. + * + * @param tenantId the tenantId + * @param edgeIds the edge Ids + * @return the list of edge objects + */ + ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds); + + /** + * Find edges by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find edges by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find edge infos by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of edge info objects + */ + PageData findEdgeInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find edge infos by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of edge info objects + */ + PageData findEdgeInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find edges by tenantId, customerId and edges Ids. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param edgeIds the edge Ids + * @return the list of edge objects + */ + ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List edgeIds); + + /** + * Find edges by tenantId and edge name. + * + * @param tenantId the tenantId + * @param name the edge name + * @return the optional edge object + */ + Optional findEdgeByTenantIdAndName(UUID tenantId, String name); + + /** + * Find tenants edge types. + * + * @return the list of tenant edge type objects + */ + ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId); + + /** + * Find edge by routing Key. + * + * @param routingKey the edge routingKey + * @return the optional edge object + */ + Optional findByRoutingKey(UUID tenantId, String routingKey); + + PageData findEdgeInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find edges by tenantId and ruleChainId. + * + * @param tenantId the tenantId + * @param ruleChainId the ruleChainId + * @return the list of rule chain objects + */ + ListenableFuture> findEdgesByTenantIdAndRuleChainId(UUID tenantId, UUID ruleChainId); + + /** + * Find edges by tenantId and dashboardId. + * + * @param tenantId the tenantId + * @param dashboardId the dashboardId + * @return the list of rule chain objects + */ + ListenableFuture> findEdgesByTenantIdAndDashboardId(UUID tenantId, UUID dashboardId); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java new file mode 100644 index 0000000000..392e7276b1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +/** + * The Interface EdgeEventDao. + */ +public interface EdgeEventDao extends Dao { + + /** + * Save or update edge event object async + * + * @param edgeEvent the event object + * @return saved edge event object future + */ + ListenableFuture saveAsync(EdgeEvent edgeEvent); + + + /** + * Find edge events by tenantId, edgeId and pageLink. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the pageLink + * @return the event list + */ + PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java new file mode 100644 index 0000000000..fe50a63907 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -0,0 +1,679 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Function; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.IdBased; +import org.thingsboard.server.common.data.id.RuleChainId; +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.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.user.UserService; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; +import static org.thingsboard.server.dao.service.Validator.validateString; + +@Service +@Slf4j +public class EdgeServiceImpl extends AbstractEntityService implements EdgeService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + private RestTemplate restTemplate; + + private static final String EDGE_LICENSE_SERVER_ENDPOINT = "https://license.thingsboard.io"; + + @Autowired + private EdgeDao edgeDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CustomerDao customerDao; + + @Autowired + private UserService userService; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private RelationService relationService; + + @Value("${edges.enabled:false}") + private boolean edgesEnabled; + + @PostConstruct + public void init() { + if (edgesEnabled) { + initRestTemplate(); + } + } + + @Override + public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findById(tenantId, edgeId.getId()); + } + + @Override + public EdgeInfo findEdgeInfoById(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeInfoById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findEdgeInfoById(tenantId, edgeId.getId()); + } + + @Override + public ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findByIdAsync(tenantId, edgeId.getId()); + } + + @Cacheable(cacheNames = EDGE_CACHE, key = "{#tenantId, #name}") + @Override + public Edge findEdgeByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findEdgeByTenantIdAndName [{}][{}]", tenantId, name); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Optional edgeOpt = edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name); + return edgeOpt.orElse(null); + } + + @Override + public Optional findEdgeByRoutingKey(TenantId tenantId, String routingKey) { + log.trace("Executing findEdgeByRoutingKey [{}]", routingKey); + Validator.validateString(routingKey, "Incorrect edge routingKey for search request."); + return edgeDao.findByRoutingKey(tenantId.getId(), routingKey); + } + + @CacheEvict(cacheNames = EDGE_CACHE, key = "{#edge.tenantId, #edge.name}") + @Override + public Edge saveEdge(Edge edge) { + log.trace("Executing saveEdge [{}]", edge); + edgeValidator.validate(edge, Edge::getTenantId); + try { + return edgeDao.save(edge.getTenantId(), edge); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null + && e.getConstraintName().equalsIgnoreCase("edge_name_unq_key")) { + throw new DataValidationException("Edge with such name already exists!"); + } else { + throw t; + } + } + } + + @Override + public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId) { + log.trace("[{}] Executing assignEdgeToCustomer [{}][{}]", tenantId, edgeId, customerId); + Edge edge = findEdgeById(tenantId, edgeId); + edge.setCustomerId(customerId); + return saveEdge(edge); + } + + @Override + public Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId) { + log.trace("[{}] Executing unassignEdgeFromCustomer [{}]", tenantId, edgeId); + Edge edge = findEdgeById(tenantId, edgeId); + edge.setCustomerId(null); + return saveEdge(edge); + } + + @Override + public void deleteEdge(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing deleteEdge [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + + List list = new ArrayList<>(); + list.add(edge.getTenantId()); + list.add(edge.getName()); + Cache cache = cacheManager.getCache(EDGE_CACHE); + cache.evict(list); + + deleteEntityRelations(tenantId, edgeId); + + edgeDao.removeById(tenantId, edgeId.getId()); + } + + @Override + public PageData findEdgesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findEdgesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findEdgesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantId(tenantId.getId(), pageLink); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(TenantId tenantId, List edgeIds) { + log.trace("Executing findEdgesByTenantIdAndIdsAsync, tenantId [{}], edgeIds [{}]", tenantId, edgeIds); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + return edgeDao.findEdgesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(edgeIds)); + } + + @Override + public void deleteEdgesByTenantId(TenantId tenantId) { + log.trace("Executing deleteEdgesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantEdgesRemover.removeEntities(tenantId, tenantId); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List edgeIds) { + log.trace("Executing findEdgesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], edgeIds [{}]", tenantId, customerId, edgeIds); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + return edgeDao.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), + customerId.getId(), toUUIDs(edgeIds)); + } + + @Override + public void unassignCustomerEdges(TenantId tenantId, CustomerId customerId) { + log.trace("Executing unassignCustomerEdges, tenantId [{}], customerId [{}]", tenantId, customerId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + customerEdgeUnassigner.removeEntities(tenantId, customerId); + } + + @Override + public ListenableFuture> findEdgesByQuery(TenantId tenantId, EdgeSearchQuery query) { + log.trace("[{}] Executing findEdgesByQuery [{}]", tenantId, query); + ListenableFuture> relations = relationService.findByQuery(tenantId, query.toEntitySearchQuery()); + ListenableFuture> edges = Futures.transformAsync(relations, r -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : r) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.EDGE) { + futures.add(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }, MoreExecutors.directExecutor()); + + edges = Futures.transform(edges, new Function, List>() { + @Nullable + @Override + public List apply(@Nullable List edgeList) { + return edgeList == null ? Collections.emptyList() : edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()); + } + }, MoreExecutors.directExecutor()); + + return edges; + } + + @Override + public ListenableFuture> findEdgeTypesByTenantId(TenantId tenantId) { + log.trace("Executing findEdgeTypesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + ListenableFuture> tenantEdgeTypes = edgeDao.findTenantEdgeTypesAsync(tenantId.getId()); + return Futures.transform(tenantEdgeTypes, + edgeTypes -> { + edgeTypes.sort(Comparator.comparing(EntitySubtype::getType)); + return edgeTypes; + }, MoreExecutors.directExecutor()); + } + + @Override + public void assignDefaultRuleChainsToEdge(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing assignDefaultRuleChainsToEdge, tenantId [{}], edgeId [{}]", tenantId, edgeId); + ListenableFuture> future = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(List ruleChains) { + if (ruleChains != null && !ruleChains.isEmpty()) { + for (RuleChain ruleChain : ruleChains) { + ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find default edge rule chains [{}]", tenantId.getId(), edgeId.getId(), t); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndRuleChainId(TenantId tenantId, RuleChainId ruleChainId) { + log.trace("Executing findEdgesByTenantIdAndRuleChainId, tenantId [{}], ruleChainId [{}]", tenantId, ruleChainId); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId); + return edgeDao.findEdgesByTenantIdAndRuleChainId(tenantId.getId(), ruleChainId.getId()); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndDashboardId(TenantId tenantId, DashboardId dashboardId) { + log.trace("Executing findEdgesByTenantIdAndDashboardId, tenantId [{}], dashboardId [{}]", tenantId, dashboardId); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + return edgeDao.findEdgesByTenantIdAndDashboardId(tenantId.getId(), dashboardId.getId()); + } + + private DataValidator edgeValidator = + new DataValidator() { + + @Override + protected void validateCreate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateUpdate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateDataImpl(TenantId tenantId, Edge edge) { + if (StringUtils.isEmpty(edge.getType())) { + throw new DataValidationException("Edge type should be specified!"); + } + if (StringUtils.isEmpty(edge.getName())) { + throw new DataValidationException("Edge name should be specified!"); + } + if (StringUtils.isEmpty(edge.getSecret())) { + throw new DataValidationException("Edge secret should be specified!"); + } + if (StringUtils.isEmpty(edge.getRoutingKey())) { + throw new DataValidationException("Edge routing key should be specified!"); + } + if (StringUtils.isEmpty(edge.getEdgeLicenseKey())) { + throw new DataValidationException("Edge license key should be specified!"); + } + if (StringUtils.isEmpty(edge.getCloudEndpoint())) { + throw new DataValidationException("Cloud endpoint should be specified!"); + } + if (edge.getTenantId() == null) { + throw new DataValidationException("Edge should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(edge.getTenantId(), edge.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Edge is referencing to non-existent tenant!"); + } + } + if (edge.getCustomerId() == null) { + edge.setCustomerId(new CustomerId(NULL_UUID)); + } else if (!edge.getCustomerId().getId().equals(NULL_UUID)) { + Customer customer = customerDao.findById(edge.getTenantId(), edge.getCustomerId().getId()); + if (customer == null) { + throw new DataValidationException("Can't assign edge to non-existent customer!"); + } + if (!customer.getTenantId().getId().equals(edge.getTenantId().getId())) { + throw new DataValidationException("Can't assign edge to customer from different tenant!"); + } + } + } + }; + + private PaginatedRemover tenantEdgesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return edgeDao.findEdgesByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Edge entity) { + deleteEdge(tenantId, new EdgeId(entity.getUuidId())); + } + }; + + private PaginatedRemover customerEdgeUnassigner = new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { + return edgeDao.findEdgesByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Edge entity) { + unassignEdgeFromCustomer(tenantId, new EdgeId(entity.getUuidId())); + } + }; + + @Override + public ListenableFuture> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId) { + // TODO: voba - rewrite 'find' to use native SQL queries instead of fetching relations + + log.trace("[{}] Executing findRelatedEdgeIdsByEntityId [{}]", tenantId, entityId); + if (EntityType.TENANT.equals(entityId.getEntityType()) || + EntityType.CUSTOMER.equals(entityId.getEntityType()) || + EntityType.DEVICE_PROFILE.equals(entityId.getEntityType())) { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + if (EntityType.TENANT.equals(entityId.getEntityType()) || + EntityType.DEVICE_PROFILE.equals(entityId.getEntityType())) { + pageData = findEdgesByTenantId(tenantId, pageLink); + } else { + pageData = findEdgesByTenantIdAndCustomerId(tenantId, new CustomerId(entityId.getId()), pageLink); + } + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + result.add(edge.getId()); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return Futures.immediateFuture(result); + } else { + switch (entityId.getEntityType()) { + case DEVICE: + case ASSET: + case ENTITY_VIEW: + ListenableFuture> originatorEdgeRelationsFuture = + relationService.findByToAndTypeAsync(tenantId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return Futures.transform(originatorEdgeRelationsFuture, originatorEdgeRelations -> { + if (originatorEdgeRelations != null && originatorEdgeRelations.size() > 0 && + originatorEdgeRelations.get(0).getFrom() != null) { + return Collections.singletonList(new EdgeId(originatorEdgeRelations.get(0).getFrom().getId())); + } else { + return Collections.emptyList(); + } + }, MoreExecutors.directExecutor()); + case DASHBOARD: + return convertToEdgeIds(findEdgesByTenantIdAndDashboardId(tenantId, new DashboardId(entityId.getId()))); + case RULE_CHAIN: + return convertToEdgeIds(findEdgesByTenantIdAndRuleChainId(tenantId, new RuleChainId(entityId.getId()))); + case USER: + User userById = userService.findUserById(tenantId, new UserId(entityId.getId())); + if (userById == null) { + return Futures.immediateFuture(Collections.emptyList()); + } + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + if (userById.getCustomerId() == null || userById.getCustomerId().isNullUid()) { + pageData = findEdgesByTenantId(tenantId, pageLink); + } else { + pageData = findEdgesByTenantIdAndCustomerId(tenantId, new CustomerId(entityId.getId()), pageLink); + } + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + result.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return convertToEdgeIds(Futures.immediateFuture(result)); + default: + return Futures.immediateFuture(Collections.emptyList()); + } + } + } + + private ListenableFuture> convertToEdgeIds(ListenableFuture> future) { + return Futures.transform(future, edges -> { + if (edges != null && !edges.isEmpty()) { + return edges.stream().map(IdBased::getId).collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public Object checkInstance(Object request) { + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/checkInstance", request, Object.class, new Object[0]); + } + + @Override + public Object activateInstance(String edgeLicenseSecret, String releaseDate) { + Map params = new HashMap(); + params.put("licenseSecret", edgeLicenseSecret); + params.put("releaseDate", releaseDate); + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/activateInstance?licenseSecret={licenseSecret}&releaseDate={releaseDate}", (Object) null, Object.class, params); + } + + @Override + public String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId) { + List edgeRuleChains = findEdgeRuleChains(tenantId, edgeId); + List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).collect(Collectors.toList()); + ObjectNode result = mapper.createObjectNode(); + for (RuleChain edgeRuleChain : edgeRuleChains) { + List connectionInfos = + ruleChainService.loadRuleChainMetaData(edgeRuleChain.getTenantId(), edgeRuleChain.getId()).getRuleChainConnections(); + if (connectionInfos != null && !connectionInfos.isEmpty()) { + List connectedRuleChains = + connectionInfos.stream().map(RuleChainConnectionInfo::getTargetRuleChainId).collect(Collectors.toList()); + List missingRuleChains = new ArrayList<>(); + for (RuleChainId connectedRuleChain : connectedRuleChains) { + if (!edgeRuleChainIds.contains(connectedRuleChain)) { + RuleChain ruleChainById = ruleChainService.findRuleChainById(tenantId, connectedRuleChain); + missingRuleChains.add(ruleChainById.getName()); + } + } + if (!missingRuleChains.isEmpty()) { + ArrayNode array = mapper.createArrayNode(); + for (String missingRuleChain : missingRuleChains) { + array.add(missingRuleChain); + } + result.set(edgeRuleChain.getName(), array); + } + } + } + return result.toString(); + } + + private List findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + result.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return result; + } + + private void initRestTemplate() { + boolean jdkHttpClientEnabled = isNotEmpty(System.getProperty("tb.proxy.jdk")) && System.getProperty("tb.proxy.jdk").equalsIgnoreCase("true"); + boolean systemProxyEnabled = isNotEmpty(System.getProperty("tb.proxy.system")) && System.getProperty("tb.proxy.system").equalsIgnoreCase("true"); + boolean proxyEnabled = isNotEmpty(System.getProperty("tb.proxy.host")) && isNotEmpty(System.getProperty("tb.proxy.port")); + if (jdkHttpClientEnabled) { + log.warn("Going to use plain JDK Http Client!"); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + factory.setProxy(new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port"))))); + } + + this.restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory()); + } else { + CloseableHttpClient httpClient; + HttpComponentsClientHttpRequestFactory requestFactory; + if (systemProxyEnabled) { + log.warn("Going to use System Proxy Server!"); + httpClient = HttpClients.createSystem(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).setProxy(new HttpHost(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port")), "https")).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else { + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } + } + + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 5ee19dce75..0c80195d9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,18 +18,45 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import java.util.List; import java.util.Optional; @Slf4j public abstract class AbstractEntityService { + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + @Autowired protected RelationService relationService; + @Autowired + protected EntityViewService entityViewService; + + @Autowired(required = false) + protected EdgeService edgeService; + + protected void createRelation(TenantId tenantId, EntityRelation relation) { + log.debug("Creating relation: {}", relation); + relationService.saveRelation(tenantId, relation); + } + + protected void deleteRelation(TenantId tenantId, EntityRelation relation) { + log.debug("Deleting relation: {}", relation); + relationService.deleteRelation(tenantId, relation); + } + protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); relationService.deleteEntityRelations(tenantId, entityId); @@ -44,4 +71,23 @@ public abstract class AbstractEntityService { return Optional.empty(); } } + + protected void checkAssignedEntityViewsToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) { + try { + List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId).get(); + if (entityViews != null && !entityViews.isEmpty()) { + EntityView entityView = entityViews.get(0); + // TODO: voba - refactor this blocking operation in 3.3+ + Boolean relationExists = relationService.checkRelation(tenantId,edgeId, entityView.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE).get(); + if (relationExists) { + throw new DataValidationException("Can't unassign device/asset from edge that is related to entity view and entity view is assigned to edge!"); + } + } + } catch (Exception e) { + log.error("[{}] Exception while finding entity views for entityId [{}]", tenantId, entityId, e); + throw new RuntimeException("Exception while finding entity views for entityId [" + entityId + "]", e); + } + } + } 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 541b7d4ec2..5dc9cf4eed 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -148,6 +149,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe case RULE_CHAIN: hasName = ruleChainService.findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId())); break; + case EDGE: + hasName = edgeService.findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId())); + break; default: throw new IllegalStateException("Not Implemented!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index 49e6e84bd9..c4a9e0d871 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -106,8 +106,8 @@ public interface EntityViewDao extends Dao { * @return the list of entity view objects */ PageData findEntityViewsByTenantIdAndCustomerId(UUID tenantId, - UUID customerId, - PageLink pageLink); + UUID customerId, + PageLink pageLink); /** * Find entity view infos by tenantId, customerId and page link. @@ -129,9 +129,9 @@ public interface EntityViewDao extends Dao { * @return the list of entity view objects */ PageData findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, - UUID customerId, - String type, - PageLink pageLink); + UUID customerId, + String type, + PageLink pageLink); /** * Find entity view infos by tenantId, customerId, type and page link. @@ -153,4 +153,30 @@ public interface EntityViewDao extends Dao { */ ListenableFuture> findTenantEntityViewTypesAsync(UUID tenantId); + /** + * Find entity views by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of entity view objects + */ + PageData findEntityViewsByTenantIdAndEdgeId(UUID tenantId, + UUID edgeId, + PageLink pageLink); + + /** + * Find entity views by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of entity view objects + */ + PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, + UUID edgeId, + String type, + PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 5b6ae32437..304b04a33d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -35,15 +35,19 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -58,6 +62,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE; @@ -77,6 +82,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_ENTITY_VIEW_ID = "Incorrect entityViewId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; @Autowired private EntityViewDao entityViewDao; @@ -328,6 +334,73 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti }, MoreExecutors.directExecutor()); } + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") + @Override + public EntityView assignEntityViewToEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId) { + EntityView entityView = findEntityViewById(tenantId, entityViewId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign entityView to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(entityView.getTenantId().getId())) { + throw new DataValidationException("Can't assign entityView to edge from different tenant!"); + } + + try { + Boolean relationExists = relationService.checkRelation(tenantId, edgeId, entityView.getEntityId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE).get(); + if (!relationExists) { + throw new DataValidationException("Can't assign entity view to edge because related device/asset doesn't assigned to edge!"); + } + } catch (ExecutionException | InterruptedException e) { + log.error("Exception during relation check", e); + throw new RuntimeException("Exception during relation check", e); + } + + try { + createRelation(tenantId, new EntityRelation(edgeId, entityViewId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create entityView relation. Edge Id: [{}]", entityViewId, edgeId); + throw new RuntimeException(e); + } + return entityView; + } + + @Override + public EntityView unassignEntityViewFromEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId) { + EntityView entityView = findEntityViewById(tenantId, entityViewId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign entityView from non-existent edge!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, entityViewId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete entityView relation. Edge Id: [{}]", entityViewId, edgeId); + throw new RuntimeException(e); + } + return entityView; + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findEntityViewsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findEntityViewsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}], pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + private DataValidator entityViewValidator = new DataValidator() { 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 3afae3c081..c47e4014e7 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 @@ -371,11 +371,13 @@ public class ModelConstants { public static final String RULE_CHAIN_COLUMN_FAMILY_NAME = "rule_chain"; public static final String RULE_CHAIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String RULE_CHAIN_NAME_PROPERTY = "name"; + public static final String RULE_CHAIN_TYPE_PROPERTY = "type"; public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id"; public static final String RULE_CHAIN_ROOT_PROPERTY = "root"; public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration"; public static final String RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_and_search_text"; + public static final String RULE_CHAIN_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_by_type_and_search_text"; /** * Cassandra rule node constants. @@ -467,6 +469,43 @@ public class ModelConstants { public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; public static final String RESOURCE_DATA_COLUMN = "data"; + /** + * Edge constants. + */ + public static final String EDGE_COLUMN_FAMILY_NAME = "edge"; + public static final String EDGE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String EDGE_ROOT_RULE_CHAIN_ID_PROPERTY = "root_rule_chain_id"; + public static final String EDGE_NAME_PROPERTY = "name"; + public static final String EDGE_LABEL_PROPERTY = "label"; + public static final String EDGE_TYPE_PROPERTY = "type"; + public static final String EDGE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_and_search_text"; + public static final String EDGE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_by_type_and_search_text"; + public static final String EDGE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_customer_and_search_text"; + public static final String EDGE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_customer_by_type_and_search_text"; + public static final String EDGE_BY_TENANT_AND_NAME_VIEW_NAME = "edge_by_tenant_and_name"; + public static final String EDGE_BY_ROUTING_KEY_VIEW_NAME = "edge_by_routing_key"; + + public static final String EDGE_ROUTING_KEY_PROPERTY = "routing_key"; + public static final String EDGE_SECRET_PROPERTY = "secret"; + public static final String EDGE_LICENSE_KEY_PROPERTY = "edge_license_key"; + public static final String EDGE_CLOUD_ENDPOINT_KEY_PROPERTY = "cloud_endpoint"; + + /** + * Edge queue constants. + */ + public static final String EDGE_EVENT_COLUMN_FAMILY_NAME = "edge_event"; + public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id"; + public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type"; + public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action"; + public static final String EDGE_EVENT_UID_PROPERTY = "edge_event_uid"; + public static final String EDGE_EVENT_ENTITY_ID_PROPERTY = "entity_id"; + public static final String EDGE_EVENT_BODY_PROPERTY = "body"; + + public static final String EDGE_EVENT_BY_ID_VIEW_NAME = "edge_event_by_id"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java new file mode 100644 index 0000000000..bb9e9d98ff --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java @@ -0,0 +1,174 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CLOUD_ENDPOINT_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LICENSE_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROOT_RULE_CHAIN_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_SECRET_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractEdgeEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = EDGE_TENANT_ID_PROPERTY, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = EDGE_CUSTOMER_ID_PROPERTY, columnDefinition = "uuid") + private UUID customerId; + + @Column(name = EDGE_ROOT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID rootRuleChainId; + + @Column(name = EDGE_TYPE_PROPERTY) + private String type; + + @Column(name = EDGE_NAME_PROPERTY) + private String name; + + @Column(name = EDGE_LABEL_PROPERTY) + private String label; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = EDGE_ROUTING_KEY_PROPERTY) + private String routingKey; + + @Column(name = EDGE_SECRET_PROPERTY) + private String secret; + + @Column(name = EDGE_LICENSE_KEY_PROPERTY) + private String edgeLicenseKey; + + @Column(name = EDGE_CLOUD_ENDPOINT_KEY_PROPERTY) + private String cloudEndpoint; + + @Type(type = "json") + @Column(name = ModelConstants.EDGE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public AbstractEdgeEntity() { + super(); + } + + public AbstractEdgeEntity(Edge edge) { + if (edge.getId() != null) { + this.setUuid(edge.getId().getId()); + } + this.setCreatedTime(edge.getCreatedTime()); + if (edge.getTenantId() != null) { + this.tenantId = edge.getTenantId().getId(); + } + if (edge.getCustomerId() != null) { + this.customerId = edge.getCustomerId().getId(); + } + if (edge.getRootRuleChainId() != null) { + this.rootRuleChainId = edge.getRootRuleChainId().getId(); + } + this.type = edge.getType(); + this.name = edge.getName(); + this.label = edge.getLabel(); + this.routingKey = edge.getRoutingKey(); + this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); + this.additionalInfo = edge.getAdditionalInfo(); + } + + public AbstractEdgeEntity(EdgeEntity edgeEntity) { + this.setId(edgeEntity.getId()); + this.setCreatedTime(edgeEntity.getCreatedTime()); + this.tenantId = edgeEntity.getTenantId(); + this.customerId = edgeEntity.getCustomerId(); + this.rootRuleChainId = edgeEntity.getRootRuleChainId(); + this.type = edgeEntity.getType(); + this.name = edgeEntity.getName(); + this.label = edgeEntity.getLabel(); + this.searchText = edgeEntity.getSearchText(); + this.routingKey = edgeEntity.getRoutingKey(); + this.secret = edgeEntity.getSecret(); + this.edgeLicenseKey = edgeEntity.getEdgeLicenseKey(); + this.cloudEndpoint = edgeEntity.getCloudEndpoint(); + this.additionalInfo = edgeEntity.getAdditionalInfo(); + } + + public String getSearchText() { + return searchText; + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + protected Edge toEdge() { + Edge edge = new Edge(new EdgeId(getUuid())); + edge.setCreatedTime(createdTime); + if (tenantId != null) { + edge.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + edge.setCustomerId(new CustomerId(customerId)); + } + if (rootRuleChainId != null) { + edge.setRootRuleChainId(new RuleChainId(rootRuleChainId)); + } + edge.setType(type); + edge.setName(name); + edge.setLabel(label); + edge.setRoutingKey(routingKey); + edge.setSecret(secret); + edge.setEdgeLicenseKey(edgeLicenseKey); + edge.setCloudEndpoint(cloudEndpoint); + edge.setAdditionalInfo(additionalInfo); + return edge; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java index 2fb67b5cf6..2181aefc7d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java @@ -25,6 +25,7 @@ import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java new file mode 100644 index 0000000000..e736c74d38 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = EDGE_COLUMN_FAMILY_NAME) +public class EdgeEntity extends AbstractEdgeEntity { + + public EdgeEntity() { + super(); + } + + public EdgeEntity(Edge edge) { + super(edge); + } + + @Override + public Edge toData() { + return super.toEdge(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java new file mode 100644 index 0000000000..2aad852c89 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ACTION_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EPOCH_DIFF; +import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = EDGE_EVENT_COLUMN_FAMILY_NAME) +@NoArgsConstructor +public class EdgeEventEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = EDGE_EVENT_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = EDGE_EVENT_EDGE_ID_PROPERTY) + private UUID edgeId; + + @Column(name = EDGE_EVENT_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Enumerated(EnumType.STRING) + @Column(name = EDGE_EVENT_TYPE_PROPERTY) + private EdgeEventType edgeEventType; + + @Enumerated(EnumType.STRING) + @Column(name = EDGE_EVENT_ACTION_PROPERTY) + private EdgeEventActionType edgeEventAction; + + @Type(type = "json") + @Column(name = EDGE_EVENT_BODY_PROPERTY) + private JsonNode entityBody; + + @Column(name = EDGE_EVENT_UID_PROPERTY) + private String edgeEventUid; + + @Column(name = TS_COLUMN) + private long ts; + + public EdgeEventEntity(EdgeEvent edgeEvent) { + if (edgeEvent.getId() != null) { + this.setUuid(edgeEvent.getId().getId()); + this.ts = getTs(edgeEvent.getId().getId()); + } else { + this.ts = System.currentTimeMillis(); + } + this.setCreatedTime(edgeEvent.getCreatedTime()); + if (edgeEvent.getTenantId() != null) { + this.tenantId = edgeEvent.getTenantId().getId(); + } + if (edgeEvent.getEdgeId() != null) { + this.edgeId = edgeEvent.getEdgeId().getId(); + } + if (edgeEvent.getEntityId() != null) { + this.entityId = edgeEvent.getEntityId(); + } + this.edgeEventType = edgeEvent.getType(); + this.edgeEventAction = edgeEvent.getAction(); + this.entityBody = edgeEvent.getBody(); + this.edgeEventUid = edgeEvent.getUid(); + } + + @Override + public EdgeEvent toData() { + EdgeEvent edgeEvent = new EdgeEvent(new EdgeEventId(this.getUuid())); + edgeEvent.setCreatedTime(createdTime); + edgeEvent.setTenantId(new TenantId(tenantId)); + edgeEvent.setEdgeId(new EdgeId(edgeId)); + if (entityId != null) { + edgeEvent.setEntityId(entityId); + } + edgeEvent.setType(edgeEventType); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setBody(entityBody); + edgeEvent.setUid(edgeEventUid); + return edgeEvent; + } + + private static long getTs(UUID uuid) { + return (uuid.timestamp() - EPOCH_DIFF) / 10000; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java new file mode 100644 index 0000000000..f7c86831f7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.edge.EdgeInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EdgeInfoEntity extends AbstractEdgeEntity { + + public static final Map edgeInfoColumnMap = new HashMap<>(); + static { + edgeInfoColumnMap.put("customerTitle", "c.title"); + } + + private String customerTitle; + private boolean customerIsPublic; + + public EdgeInfoEntity() { + super(); + } + + public EdgeInfoEntity(EdgeEntity edgeEntity, + String customerTitle, + Object customerAdditionalInfo) { + super(edgeEntity); + this.customerTitle = customerTitle; + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); + } else { + this.customerIsPublic = false; + } + } + + @Override + public EdgeInfo toData() { + return new EdgeInfo(super.toEdge(), customerTitle, customerIsPublic); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java index dd5bbfc10b..d8e820a87e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java @@ -25,10 +25,6 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Entity; import javax.persistence.Table; -/** - * Created by Victor Basanets on 8/30/2017. - */ - @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 3909d55c44..be195723bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -32,6 +33,8 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.Table; import java.util.UUID; @@ -48,6 +51,10 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) private String name; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) + private RuleChainType type; + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; @@ -78,6 +85,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT this.setCreatedTime(ruleChain.getCreatedTime()); this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); this.name = ruleChain.getName(); + this.type = ruleChain.getType(); this.searchText = ruleChain.getName(); if (ruleChain.getFirstRuleNodeId() != null) { this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); @@ -104,6 +112,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT ruleChain.setCreatedTime(createdTime); ruleChain.setTenantId(new TenantId(tenantId)); ruleChain.setName(name); + ruleChain.setType(type); if (firstRuleNodeId != null) { ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java index 0f1b39a459..0df2f19e80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; -import org.thingsboard.server.common.data.lwm2m.LwM2mResource; +import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -87,7 +87,9 @@ public class BaseTbResourceService implements TbResourceService { String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); String name = objectModel.name; resource.setResourceKey(resourceKey); - resource.setTitle(name + " id=" +objectModel.id + " v" + objectModel.getVersion()); + if (resource.getId() == null) { + resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.getVersion()); + } resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); } else { throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); @@ -205,14 +207,14 @@ public class BaseTbResourceService implements TbResourceService { lwM2mObject.setMandatory(obj.mandatory); LwM2mInstance instance = new LwM2mInstance(); instance.setId(0); - List resources = new ArrayList<>(); + List resources = new ArrayList<>(); obj.resources.forEach((k, v) -> { if (!v.operations.isExecutable()) { - LwM2mResource lwM2mResource = new LwM2mResource(k, v.name, false, false, false); - resources.add(lwM2mResource); + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); } }); - instance.setResources(resources.toArray(LwM2mResource[]::new)); + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); lwM2mObject.setInstances(new LwM2mInstance[]{instance}); return lwM2mObject; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 5eb0bc190a..b85a257ed2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -29,6 +29,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -44,6 +46,7 @@ import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -66,6 +69,7 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.TENANT; +import static org.thingsboard.server.dao.service.Validator.validateId; /** * Created by igor on 3/12/18. @@ -75,6 +79,9 @@ import static org.thingsboard.server.common.data.DataConstants.TENANT; public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { private static final int DEFAULT_PAGE_SIZE = 1000; + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + @Autowired private RuleChainDao ruleChainDao; @@ -96,9 +103,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC try { createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create tenant to root rule chain relation. from: [{}], to: [{}]", - savedRuleChain.getTenantId(), savedRuleChain.getId()); + savedRuleChain.getTenantId(), savedRuleChain.getTenantId(), savedRuleChain.getId(), e); throw new RuntimeException(e); } } @@ -180,7 +187,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC try { createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create rule chain to rule node relation. from: [{}], to: [{}]", ruleChainMetaData.getRuleChainId(), savedNode.getId()); throw new RuntimeException(e); @@ -208,7 +215,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC String type = nodeConnection.getType(); try { createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create rule node relation. from: [{}], to: [{}]", from, to); throw new RuntimeException(e); } @@ -221,7 +228,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC String type = nodeToRuleChainConnection.getType(); try { createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to); throw new RuntimeException(e); } @@ -318,12 +325,21 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChain getRootTenantRuleChain(TenantId tenantId) { + return getRootRuleChainByType(tenantId, RuleChainType.CORE); + } + + private RuleChain getRootRuleChainByType(TenantId tenantId, RuleChainType type) { Validator.validateId(tenantId, "Incorrect tenant id for search request."); List relations = relationService.findByFrom(tenantId, tenantId, RelationTypeGroup.RULE_CHAIN); if (relations != null && !relations.isEmpty()) { - EntityRelation relation = relations.get(0); - RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId()); - return findRuleChainById(tenantId, ruleChainId); + for (EntityRelation relation : relations) { + RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId()); + RuleChain ruleChainById = findRuleChainById(tenantId, ruleChainId); + if (type.equals(ruleChainById.getType())) { + return ruleChainById; + } + } + return null; } else { return null; } @@ -387,18 +403,34 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink) { + public PageData findTenantRuleChainsByType(TenantId tenantId, RuleChainType type, PageLink pageLink) { Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); Validator.validatePageLink(pageLink); - return ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); + return ruleChainDao.findRuleChainsByTenantIdAndType(tenantId.getId(), type, pageLink); } @Override public void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request."); RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); - if (ruleChain != null && ruleChain.isRoot()) { - throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!"); + if (ruleChain != null) { + if (ruleChain.isRoot()) { + throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!"); + } + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + try { + List edges = edgeService.findEdgesByTenantIdAndRuleChainId(tenantId, ruleChainId).get(); + if (edges != null && !edges.isEmpty()) { + for (Edge edge : edges) { + if (edge.getRootRuleChainId() != null && edge.getRootRuleChainId().equals(ruleChainId)) { + throw new DataValidationException("Can't delete rule chain that is root for edge [" + edge.getName() + "]. Please assign another root rule chain first to the edge!"); + } + } + } + } catch (InterruptedException | ExecutionException e) { + log.error("Can't get edges by tenant id [{}] and rule chain id [{}]", tenantId.getId(), ruleChainId.getId(), e); + } + } } checkRuleNodesAndDelete(tenantId, ruleChainId); } @@ -425,13 +457,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) { + public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite) { List importResults = new ArrayList<>(); setRandomRuleChainIds(ruleChainData); resetRuleNodeIds(ruleChainData.getMetadata()); resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata()); if (overwrite) { - List persistentRuleChains = findAllTenantRuleChains(tenantId); + List persistentRuleChains = findAllTenantRuleChains(tenantId, type); for (RuleChain ruleChain : ruleChainData.getRuleChains()) { ComponentLifecycleEvent lifecycleEvent; Optional persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst(); @@ -511,19 +543,19 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } - private List findAllTenantRuleChains(TenantId tenantId) { + private List findAllTenantRuleChains(TenantId tenantId, RuleChainType type) { PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), pageLink); + return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), type, pageLink); } - private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, PageLink pageLink) { - PageData persistentRuleChainData = findTenantRuleChains(tenantId, pageLink); + private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, RuleChainType type, PageLink pageLink) { + PageData persistentRuleChainData = findTenantRuleChainsByType(tenantId, type, pageLink); List ruleChains = persistentRuleChainData.getData(); if (!CollectionUtils.isEmpty(ruleChains)) { accumulator.addAll(ruleChains); } if (persistentRuleChainData.hasNext()) { - return findAllTenantRuleChainsRecursive(tenantId, accumulator, pageLink.nextPageLink()); + return findAllTenantRuleChainsRecursive(tenantId, accumulator, type, pageLink.nextPageLink()); } return accumulator; } @@ -544,6 +576,115 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } + @Override + public RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId) { + RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign ruleChain to non-existent edge!"); + } + if (!edge.getTenantId().equals(ruleChain.getTenantId())) { + throw new DataValidationException("Can't assign ruleChain to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, ruleChainId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create ruleChain relation. Edge Id: [{}]", ruleChainId, edgeId); + throw new RuntimeException(e); + } + return ruleChain; + } + + @Override + public RuleChain unassignRuleChainFromEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId, boolean remove) { + RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign rule chain from non-existent edge!"); + } + if (!remove && edge.getRootRuleChainId() != null && edge.getRootRuleChainId().equals(ruleChainId)) { + throw new DataValidationException("Can't unassign root rule chain from edge [" + edge.getName() + "]. Please assign another root rule chain first!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, ruleChainId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete rule chain relation. Edge Id: [{}]", ruleChainId, edgeId); + throw new RuntimeException(e); + } + return ruleChain; + } + + @Override + public PageData findRuleChainsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findRuleChainsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(edgeId, "Incorrect edgeId " + edgeId); + Validator.validatePageLink(pageLink); + return ruleChainDao.findRuleChainsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public RuleChain getEdgeTemplateRootRuleChain(TenantId tenantId) { + return getRootRuleChainByType(tenantId, RuleChainType.EDGE); + } + + @Override + public boolean setEdgeTemplateRootRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); + RuleChain previousEdgeTemplateRootRuleChain = getEdgeTemplateRootRuleChain(ruleChain.getTenantId()); + if (previousEdgeTemplateRootRuleChain == null || !previousEdgeTemplateRootRuleChain.getId().equals(ruleChain.getId())) { + try { + if (previousEdgeTemplateRootRuleChain != null) { + deleteRelation(tenantId, new EntityRelation(previousEdgeTemplateRootRuleChain.getTenantId(), previousEdgeTemplateRootRuleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + previousEdgeTemplateRootRuleChain.setRoot(false); + ruleChainDao.save(tenantId, previousEdgeTemplateRootRuleChain); + } + createRelation(tenantId, new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + ruleChain.setRoot(true); + ruleChainDao.save(tenantId, ruleChain); + return true; + } catch (Exception e) { + log.warn("Failed to set edge template root rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + return false; + } + + @Override + public boolean setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + try { + createRelation(tenantId, new EntityRelation(tenantId, ruleChainId, + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN)); + return true; + } catch (Exception e) { + log.warn("Failed to set auto assign to edge rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + + @Override + public boolean unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + try { + deleteRelation(tenantId, new EntityRelation(tenantId, ruleChainId, + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN)); + return true; + } catch (Exception e) { + log.warn("Failed to unset auto assign to edge rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + + @Override + public ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId) { + log.trace("Executing findAutoAssignToEdgeRuleChainsByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return ruleChainDao.findAutoAssignToEdgeRuleChainsByTenantId(tenantId.getId()); + } + + private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { try{ ruleChainDao.removeById(tenantId, ruleChainId.getId()); @@ -575,15 +716,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeDao.removeById(tenantId, entityId.getId()); } - private void createRelation(TenantId tenantId, EntityRelation relation) throws ExecutionException, InterruptedException { - log.debug("Creating relation: {}", relation); - relationService.saveRelation(tenantId, relation); - } - - private void deleteRelation(TenantId tenantId, EntityRelation relation) throws ExecutionException, InterruptedException { - log.debug("Deleting relation: {}", relation); - relationService.deleteRelation(tenantId, relation); - } private DataValidator ruleChainValidator = new DataValidator() { @@ -598,7 +730,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override protected void validateDataImpl(TenantId tenantId, RuleChain ruleChain) { if (StringUtils.isEmpty(ruleChain.getName())) { - throw new DataValidationException("Rule chain name should be specified!."); + throw new DataValidationException("Rule chain name should be specified!"); + } + if (ruleChain.getType() == null) { + ruleChain.setType(RuleChainType.CORE); } if (ruleChain.getTenantId() == null || ruleChain.getTenantId().isNullUid()) { throw new DataValidationException("Rule chain should be assigned to tenant!"); @@ -607,12 +742,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (tenant == null) { throw new DataValidationException("Rule chain is referencing to non-existent tenant!"); } - if (ruleChain.isRoot()) { + if (ruleChain.isRoot() && RuleChainType.CORE.equals(ruleChain.getType())) { RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId()); if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) { throw new DataValidationException("Another root rule chain is present in scope of current tenant!"); } } + if (ruleChain.isRoot() && RuleChainType.EDGE.equals(ruleChain.getType())) { + RuleChain edgeTemplateRootRuleChain = getEdgeTemplateRootRuleChain(ruleChain.getTenantId()); + if (edgeTemplateRootRuleChain != null && !edgeTemplateRootRuleChain.getId().equals(ruleChain.getId())) { + throw new DataValidationException("Another edge template root rule chain is present in scope of current tenant!"); + } + } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 30c06f95de..4baa3bd576 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.dao.rule; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; +import java.util.List; import java.util.UUID; /** @@ -36,4 +39,32 @@ public interface RuleChainDao extends Dao, TenantEntityDao { * @return the list of rule chain objects */ PageData findRuleChainsByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find rule chains by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of rule chain objects + */ + PageData findRuleChainsByTenantIdAndType(UUID tenantId, RuleChainType type, PageLink pageLink); + + /** + * Find rule chains by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of rule chain objects + */ + PageData findRuleChainsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find auto assign to edge rule chains by tenantId. + * + * @param tenantId the tenantId + * @return the list of rule chain objects + */ + ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e8b8eaf009..ff8e79efc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -17,29 +17,50 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.exception.DataValidationException; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; @Slf4j public abstract class DataValidator> { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE); + private static Validator fieldsValidator; + + static { + initializeFieldsValidator(); + } + public void validate(D data, Function tenantIdFunction) { try { if (data == null) { throw new DataValidationException("Data object can't be null!"); } + + List validationErrors = validateFields(data); + if (!validationErrors.isEmpty()) { + throw new IllegalArgumentException("Validation error: " + String.join(", ", validationErrors)); + } + TenantId tenantId = tenantIdFunction.apply(data); validateDataImpl(tenantId, data); if (data.getId() == null) { @@ -81,6 +102,14 @@ public abstract class DataValidator> { return emailMatcher.matches(); } + private List validateFields(D data) { + Set> constraintsViolations = fieldsValidator.validate(data); + return constraintsViolations.stream() + .map(ConstraintViolation::getMessage) + .distinct() + .collect(Collectors.toList()); + } + protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, TenantEntityDao tenantEntityDao, long maxEntities, @@ -111,4 +140,13 @@ public abstract class DataValidator> { throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!"); } } + + private static void initializeFieldsValidator() { + HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure(); + ConstraintMapping constraintMapping = validatorConfiguration.createConstraintMapping(); + constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class); + validatorConfiguration.addMapping(constraintMapping); + + fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator(); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java new file mode 100644 index 0000000000..e16aebbfea --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2021 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.service; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.owasp.validator.html.AntiSamy; +import org.owasp.validator.html.Policy; +import org.owasp.validator.html.PolicyException; +import org.owasp.validator.html.ScanException; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +@Slf4j +public class NoXssValidator implements ConstraintValidator { + private static final AntiSamy xssChecker = new AntiSamy(); + private static Policy xssPolicy; + + @Override + public void initialize(NoXss constraintAnnotation) { + if (xssPolicy == null) { + try { + xssPolicy = Policy.getInstance(Resources.getResource("xss-policy.xml")); + } catch (Exception e) { + log.error("Failed to set xss policy: {}", e.getMessage()); + } + } + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + if (!(value instanceof String) || ((String) value).isEmpty() || xssPolicy == null) { + return true; + } + + try { + return xssChecker.scan((String) value, xssPolicy).getNumberOfErrors() == 0; + } catch (ScanException | PolicyException e) { + return false; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index e49f09aaaa..89d1006018 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.model.sql.RuleChainEntity; import java.util.List; import java.util.UUID; @@ -122,5 +123,25 @@ public interface AssetRepository extends PagingAndSortingRepository findTenantAssetTypes(@Param("tenantId") UUID tenantId); + @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); + Long countByTenantIdAndTypeIsNot(UUID tenantId, String type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index cdd5a4c66b..2f8dbe5b02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.dao.sql.asset; +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.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -23,13 +26,18 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import java.util.ArrayList; @@ -45,6 +53,7 @@ import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE * Created by Valerii Sosliuk on 5/19/2017. */ @Component +@Slf4j public class JpaAssetDao extends JpaAbstractSearchTextDao implements AssetDao { @Autowired @@ -179,6 +188,29 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im return list; } + @Override + public PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find assets by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(assetRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find assets by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(assetRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + @Override public Long countByTenantId(TenantId tenantId) { return assetRepository.countByTenantIdAndTypeIsNot(tenantId.getId(), TB_SERVICE_QUEUE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 803c805b63..6bbf3a1d61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -44,4 +44,13 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 6d026327dd..4c34f6148a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -39,10 +39,10 @@ import java.util.UUID; public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao implements DashboardInfoDao { @Autowired - private RelationDao relationDao; + private DashboardInfoRepository dashboardInfoRepository; @Autowired - private DashboardInfoRepository dashboardInfoRepository; + private RelationDao relationDao; @Override protected Class getEntityClass() { @@ -72,4 +72,15 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find dashboards by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(dashboardInfoRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 708e0f38e3..b98a57d248 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -170,6 +170,26 @@ public interface DeviceRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT d FROM DeviceEntity d, RelationEntity re WHERE d.tenantId = :tenantId " + + "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); + Long countByTenantId(UUID tenantId); @Query("SELECT d.id FROM DeviceEntity d " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index cb91ffac4f..dc2b749926 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.device; import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.model.sql.DeviceEntity; @@ -45,6 +47,7 @@ import java.util.UUID; * Created by Valerii Sosliuk on 5/6/2017. */ @Component +@Slf4j public class JpaDeviceDao extends JpaAbstractSearchTextDao implements DeviceDao { @Autowired @@ -240,4 +243,27 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao } return list; } + + @Override + public PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find devices by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(deviceRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDevicesByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find devices by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(deviceRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java new file mode 100644 index 0000000000..4962b2e8d2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2021 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.sql.edge; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.EdgeEventEntity; + +import java.util.UUID; + +public interface EdgeEventRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { + + @Query("SELECT e FROM EdgeEventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.edgeId = :edgeId " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + ) + Page findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); + + @Query("SELECT e FROM EdgeEventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.edgeId = :edgeId " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + + "AND e.edgeEventAction <> 'TIMESERIES_UPDATED'" + ) + Page findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java new file mode 100644 index 0000000000..a5216033b3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2021 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.sql.edge; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; + +import java.util.List; +import java.util.UUID; + +public interface EdgeRepository extends PagingAndSortingRepository { + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.id = :edgeId") + EdgeInfoEntity findEdgeInfoById(@Param("edgeId") UUID edgeId); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.tenantId = :tenantId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.tenantId = :tenantId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(a, c.title, c.additionalInfo) " + + "FROM EdgeEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findEdgeInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(a, c.title, c.additionalInfo) " + + "FROM EdgeEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT DISTINCT d.type FROM EdgeEntity d WHERE d.tenantId = :tenantId") + List findTenantEdgeTypes(@Param("tenantId") UUID tenantId); + + EdgeEntity findByTenantIdAndName(UUID tenantId, String name); + + List findEdgesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List edgeIds); + + List findEdgesByTenantIdAndIdIn(UUID tenantId, List edgeIds); + + EdgeEntity findByRoutingKey(String routingKey); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java new file mode 100644 index 0000000000..9dc5c7a9a2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2021 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.sql.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.edge.EdgeEventDao; +import org.thingsboard.server.dao.model.sql.EdgeEventEntity; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Optional; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +@Slf4j +@Component +public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao implements EdgeEventDao { + + private final UUID systemTenantId = NULL_UUID; + + @Autowired + private EdgeEventRepository edgeEventRepository; + + @Override + protected Class getEntityClass() { + return EdgeEventEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return edgeEventRepository; + } + + @Override + public ListenableFuture saveAsync(EdgeEvent edgeEvent) { + log.debug("Save edge event [{}] ", edgeEvent); + if (edgeEvent.getId() == null) { + UUID timeBased = Uuids.timeBased(); + edgeEvent.setId(new EdgeEventId(timeBased)); + edgeEvent.setCreatedTime(Uuids.unixTimestamp(timeBased)); + } else if (edgeEvent.getCreatedTime() == 0L) { + UUID eventId = edgeEvent.getId().getId(); + if (eventId.version() == 1) { + edgeEvent.setCreatedTime(Uuids.unixTimestamp(eventId)); + } else { + edgeEvent.setCreatedTime(System.currentTimeMillis()); + } + } + if (StringUtils.isEmpty(edgeEvent.getUid())) { + edgeEvent.setUid(edgeEvent.getId().toString()); + } + return service.submit(() -> save(new EdgeEventEntity(edgeEvent)).orElse(null)); + } + + @Override + public PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { + if (withTsUpdate) { + return DaoUtil.toPageData( + edgeEventRepository + .findEdgeEventsByTenantIdAndEdgeId( + tenantId, + edgeId.getId(), + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); + } else { + return DaoUtil.toPageData( + edgeEventRepository + .findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated( + tenantId, + edgeId.getId(), + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); + + } + } + + public Optional save(EdgeEventEntity entity) { + log.debug("Save edge event [{}] ", entity); + if (entity.getTenantId() == null) { + log.trace("Save system edge event with predefined id {}", systemTenantId); + entity.setTenantId(systemTenantId); + } + if (entity.getUuid() == null) { + entity.setUuid(Uuids.timeBased()); + } + return Optional.of(DaoUtil.getData(edgeEventRepository.save(entity))); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java new file mode 100644 index 0000000000..f17196fe93 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -0,0 +1,218 @@ +/** + * Copyright © 2016-2021 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.sql.edge; + +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; +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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.edge.EdgeDao; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; +import org.thingsboard.server.dao.relation.RelationDao; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@Component +@Slf4j +public class JpaEdgeDao extends JpaAbstractSearchTextDao implements EdgeDao { + + @Autowired + private EdgeRepository edgeRepository; + + @Autowired + private RelationDao relationDao; + + @Override + protected Class getEntityClass() { + return EdgeEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return edgeRepository; + } + + @Override + public EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId) { + return DaoUtil.getData(edgeRepository.findEdgeInfoById(edgeId)); + } + + @Override + public PageData findEdgesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds) { + return service.submit(() -> DaoUtil.convertDataList(edgeRepository.findEdgesByTenantIdAndIdIn(tenantId, edgeIds))); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List edgeIds) { + return service.submit(() -> DaoUtil.convertDataList( + edgeRepository.findEdgesByTenantIdAndCustomerIdAndIdIn(tenantId, customerId, edgeIds))); + } + + @Override + public Optional findEdgeByTenantIdAndName(UUID tenantId, String name) { + Edge edge = DaoUtil.getData(edgeRepository.findByTenantIdAndName(tenantId, name)); + return Optional.ofNullable(edge); + } + + @Override + public PageData findEdgesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId) { + return service.submit(() -> convertTenantEdgeTypesToDto(tenantId, edgeRepository.findTenantEdgeTypes(tenantId))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public PageData findEdgeInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public Optional findByRoutingKey(UUID tenantId, String routingKey) { + Edge edge = DaoUtil.getData(edgeRepository.findByRoutingKey(routingKey)); + return Optional.ofNullable(edge); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndRuleChainId(UUID tenantId, UUID ruleChainId) { + log.debug("Try to find edges by tenantId [{}], ruleChainId [{}]", tenantId, ruleChainId); + ListenableFuture> relations = relationDao.findAllByToAndType(new TenantId(tenantId), new RuleChainId(ruleChainId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return transformFromRelationToEdge(tenantId, relations); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndDashboardId(UUID tenantId, UUID dashboardId) { + log.debug("Try to find edges by tenantId [{}], dashboardId [{}]", tenantId, dashboardId); + ListenableFuture> relations = relationDao.findAllByToAndType(new TenantId(tenantId), new DashboardId(dashboardId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return transformFromRelationToEdge(tenantId, relations); + } + + private ListenableFuture> transformFromRelationToEdge(UUID tenantId, ListenableFuture> relations) { + return Futures.transformAsync(relations, input -> { + List> edgeFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + edgeFutures.add(findByIdAsync(new TenantId(tenantId), relation.getFrom().getId())); + } + return Futures.successfulAsList(edgeFutures); + }, MoreExecutors.directExecutor()); + } + + private List convertTenantEdgeTypesToDto(UUID tenantId, List types) { + List list = Collections.emptyList(); + if (types != null && !types.isEmpty()) { + list = new ArrayList<>(); + for (String type : types) { + list.add(new EntitySubtype(new TenantId(tenantId), EntityType.EDGE, type)); + } + } + return list; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index abdd2c2e19..9e33b6e701 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; @@ -119,4 +120,24 @@ public interface EntityViewRepository extends PagingAndSortingRepository findTenantEntityViewTypes(@Param("tenantId") UUID tenantId); + + @Query("SELECT ev FROM EntityViewEntity ev, RelationEntity re WHERE ev.tenantId = :tenantId " + + "AND ev.id = re.toId AND re.toType = 'ENTITY_VIEW' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(ev.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT ev FROM EntityViewEntity ev, RelationEntity re WHERE ev.tenantId = :tenantId " + + "AND ev.id = re.toId AND re.toType = 'ENTITY_VIEW' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND ev.type = :type " + + "AND LOWER(ev.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 05a61307ec..35f81a860c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.entityview; import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -43,6 +44,7 @@ import java.util.UUID; * Created by Victor Basanets on 8/31/2017. */ @Component +@Slf4j public class JpaEntityViewDao extends JpaAbstractSearchTextDao implements EntityViewDao { @@ -175,4 +177,27 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao findEntityViewsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(entityViewRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(entityViewRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 4907c03dea..ac4d2b9553 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.AssetTypeFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -112,6 +114,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select customer_id from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select customer_id from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select customer_id from edge where id = entity_id)" + " END as customer_id"; private static final String SELECT_TENANT_ID = "SELECT CASE" + " WHEN entity.entity_type = 'TENANT' THEN entity_id" + @@ -127,6 +131,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select tenant_id from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select tenant_id from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select tenant_id from edge where id = entity_id)" + " END as tenant_id"; private static final String SELECT_CREATED_TIME = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -143,6 +149,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select created_time from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select created_time from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select created_time from edge where id = entity_id)" + " END as created_time"; private static final String SELECT_NAME = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -159,6 +167,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select name from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select name from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select name from edge where id = entity_id)" + " END as name"; private static final String SELECT_TYPE = " CASE" + " WHEN entity.entity_type = 'USER'" + @@ -169,6 +179,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select type from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select type from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select type from edge where id = entity_id)" + " ELSE entity.entity_type END as type"; private static final String SELECT_LABEL = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -185,6 +197,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select label from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select name from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select label from edge where id = entity_id)" + " END as label"; private static final String SELECT_ADDITIONAL_INFO = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -201,6 +215,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select additional_info from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select additional_info from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select additional_info from edge where id = entity_id)" + " END as additional_info"; private static final String SELECT_API_USAGE_STATE = "(select aus.id, aus.created_time, aus.tenant_id, '13814000-1dd2-11b2-8080-808080808080'::uuid as customer_id, " + @@ -215,6 +231,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.USER, "tb_user"); entityTableMap.put(EntityType.TENANT, "tenant"); entityTableMap.put(EntityType.API_USAGE_STATE, SELECT_API_USAGE_STATE); + entityTableMap.put(EntityType.EDGE, "edge"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ @@ -447,6 +464,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case DEVICE_SEARCH_QUERY: case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: + case EDGE_SEARCH_QUERY: return this.defaultPermissionQuery(ctx); default: if (ctx.getEntityType() == EntityType.TENANT) { @@ -483,11 +501,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ASSET_TYPE: case DEVICE_TYPE: case ENTITY_VIEW_TYPE: + case EDGE_TYPE: return this.typeQuery(ctx, entityFilter); case RELATIONS_QUERY: case DEVICE_SEARCH_QUERY: case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: + case EDGE_SEARCH_QUERY: case API_USAGE_STATE: case ENTITY_TYPE: return ""; @@ -509,6 +529,9 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ENTITY_VIEW_SEARCH_QUERY: EntityViewSearchQueryFilter entityViewQuery = (EntityViewSearchQueryFilter) entityFilter; return entitySearchQuery(ctx, entityViewQuery, EntityType.ENTITY_VIEW, entityViewQuery.getEntityViewTypes()); + case EDGE_SEARCH_QUERY: + EdgeSearchQueryFilter edgeQuery = (EdgeSearchQueryFilter) entityFilter; + return entitySearchQuery(ctx, edgeQuery, EntityType.EDGE, edgeQuery.getEdgeTypes()); default: return entityTableMap.get(ctx.getEntityType()); } @@ -714,6 +737,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { type = ((EntityViewTypeFilter) filter).getEntityViewType(); name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter(); break; + case EDGE_TYPE: + type = ((EdgeTypeFilter) filter).getEdgeType(); + name = ((EdgeTypeFilter) filter).getEdgeNameFilter(); + break; default: throw new RuntimeException("Not supported!"); } @@ -741,6 +768,9 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ENTITY_VIEW_TYPE: case ENTITY_VIEW_SEARCH_QUERY: return EntityType.ENTITY_VIEW; + case EDGE_TYPE: + case EDGE_SEARCH_QUERY: + return EntityType.EDGE; case RELATIONS_QUERY: return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType(); case API_USAGE_STATE: diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 9ed515f518..a759d0de74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.sql.query; import lombok.Data; -import org.springframework.util.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; @@ -42,7 +42,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -245,8 +244,9 @@ public class EntityKeyMapping { entityTypeStr = "'" + entityType.name() + "'"; } ctx.addStringParameter(alias + "_key_id", entityKey.getKey()); - String filterQuery = toQueries(ctx, entityFilter.getType()).filter(Objects::nonNull).collect( - Collectors.joining(" and ")); + String filterQuery = toQueries(ctx, entityFilter.getType()) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" and ")); if (StringUtils.isEmpty(filterQuery)) { filterQuery = ""; } else { @@ -293,8 +293,10 @@ public class EntityKeyMapping { } public static String buildQuery(QueryContext ctx, List mappings, EntityFilterType filterType) { - return mappings.stream().flatMap(mapping -> mapping.toQueries(ctx, filterType)).filter(Objects::nonNull).collect( - Collectors.joining(" AND ")); + return mappings.stream() + .flatMap(mapping -> mapping.toQueries(ctx, filterType)) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" AND ")); } public static List prepareKeyMapping(EntityDataQuery query) { @@ -461,9 +463,8 @@ public class EntityKeyMapping { ComplexFilterPredicate predicate, EntityFilterType filterType) { String result = predicate.getPredicates().stream() .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) - .filter(Objects::nonNull).collect(Collectors.joining( - " " + predicate.getOperation().name() + " " - )); + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" " + predicate.getOperation().name() + " ")); if (!result.trim().isEmpty()) { result = "( " + result + " )"; } @@ -520,7 +521,7 @@ public class EntityKeyMapping { String paramName = getNextParameterName(field); String value = stringFilterPredicate.getValue().getValue(); if (value.isEmpty()) { - return null; + return ""; } String stringOperationQuery = ""; if (stringFilterPredicate.isIgnoreCase()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 66907fea5f..760d9e7aeb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.dao.sql.rule; +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.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; @@ -22,12 +25,19 @@ import org.springframework.stereotype.Component; 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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleChainEntity; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -38,6 +48,9 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao getEntityClass() { return RuleChainEntity.class; @@ -50,6 +63,7 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findRuleChainsByTenantId(UUID tenantId, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink); return DaoUtil.toPageData(ruleChainRepository .findByTenantId( tenantId, @@ -57,6 +71,47 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findRuleChainsByTenantIdAndType(UUID tenantId, RuleChainType type, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink); + return DaoUtil.toPageData(ruleChainRepository + .findByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findRuleChainsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + + return DaoUtil.toPageData(ruleChainRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId) { + log.debug("Try to find auto assign to edge rule chains by tenantId [{}]", tenantId); + ListenableFuture> relations = + relationDao.findAllByFromAndType(new TenantId(tenantId), new TenantId(tenantId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN); + return Futures.transformAsync(relations, input -> { + if (input != null && !input.isEmpty()) { + List> ruleChainsFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + ruleChainsFutures.add(findByIdAsync(new TenantId(tenantId), relation.getTo().getId())); + } + return Futures.successfulAsList(ruleChainsFutures); + } else { + return Futures.immediateFuture(Collections.emptyList()); + } + }, MoreExecutors.directExecutor()); + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index bf37aaafd6..3c3c5ff3b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -20,6 +20,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import org.thingsboard.server.dao.model.sql.RuleChainEntity; import java.util.UUID; @@ -32,5 +34,21 @@ public interface RuleChainRepository extends PagingAndSortingRepository findByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") RuleChainType type, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT rc FROM RuleChainEntity rc, RelationEntity re WHERE rc.tenantId = :tenantId " + + "AND rc.id = re.toId AND re.toType = 'RULE_CHAIN' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); Long countByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java index b36af1b0e6..981f859ca1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java @@ -33,7 +33,7 @@ public interface TsKvTimescaleRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "AND tskv.ts >= :startTs AND tskv.ts < :endTs") List findAllWithLimit( @Param("entityId") UUID entityId, @Param("entityKey") int key, @@ -44,7 +44,7 @@ public interface TsKvTimescaleRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "AND tskv.ts >= :startTs AND tskv.ts < :endTs") void delete(@Param("entityId") UUID entityId, @Param("entityKey") int key, @Param("startTs") long startTs, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java index 8a59bc9c29..db5534d73e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java @@ -32,7 +32,7 @@ import java.util.concurrent.CompletableFuture; public interface TsKvRepository extends CrudRepository { @Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " + - "AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs") + "AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") List findAllWithLimit(@Param("entityId") UUID entityId, @Param("entityKey") int key, @Param("startTs") long startTs, @@ -42,7 +42,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") void delete(@Param("entityId") UUID entityId, @Param("entityKey") int key, @Param("startTs") long startTs, @@ -51,7 +51,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findStringMax(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @@ -63,7 +63,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findNumericMax(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @@ -73,7 +73,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findStringMin(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @@ -85,7 +85,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findNumericMin( @Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @@ -98,7 +98,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findCount(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @@ -110,7 +110,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findAvg(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @@ -122,7 +122,7 @@ public interface TsKvRepository extends CrudRepository :startTs AND tskv.ts <= :endTs") + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") CompletableFuture findSum(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 96a6d14c42..9a08c65fef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -141,6 +141,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe assetService.deleteAssetsByTenantId(tenantId); deviceService.deleteDevicesByTenantId(tenantId); deviceProfileService.deleteDeviceProfilesByTenantId(tenantId); + edgeService.deleteEdgesByTenantId(tenantId); userService.deleteTenantAdmins(tenantId); ruleChainService.deleteRuleChainsByTenantId(tenantId); apiUsageStateService.deleteApiUsageStateByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 2f817634c3..7d09578dd1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -550,8 +550,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " > ? " - + "AND " + ModelConstants.TS_COLUMN + " <= ?"); + + "AND " + ModelConstants.TS_COLUMN + " >= ? " + + "AND " + ModelConstants.TS_COLUMN + " < ?"); } return deleteStmt; } @@ -740,8 +740,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " > ? " - + "AND " + ModelConstants.TS_COLUMN + " <= ?" + + "AND " + ModelConstants.TS_COLUMN + " >= ? " + + "AND " + ModelConstants.TS_COLUMN + " < ?" + (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : "")); } } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 3fc69ff536..749f3f9aa3 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -127,6 +127,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( additional_info varchar, configuration varchar(10000000), name varchar(255), + type varchar(255), first_rule_node_id uuid, root boolean, debug_mode boolean, @@ -340,7 +341,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); - CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, enabled boolean, @@ -437,3 +437,35 @@ CREATE TABLE IF NOT EXISTS resource ( data varchar, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); + +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) +); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 56f0d580eb..7d65e4f8dc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -145,6 +145,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( additional_info varchar, configuration varchar(10000000), name varchar(255), + type varchar(255), first_rule_node_id uuid, root boolean, debug_mode boolean, @@ -464,6 +465,38 @@ CREATE TABLE IF NOT EXISTS resource ( CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) +); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ @@ -497,3 +530,20 @@ BEGIN END; $$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE cleanup_edge_events_by_ttl(IN ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE + ttl_ts bigint; + ttl_deleted_count bigint DEFAULT 0; +BEGIN + IF ttl > 0 THEN + ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint; + EXECUTE format( + 'WITH deleted AS (DELETE FROM edge_event WHERE ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', ttl_ts) into ttl_deleted_count; + END IF; + RAISE NOTICE 'Edge events removed by ttl: %', ttl_deleted_count; + deleted := ttl_deleted_count; +END +$$; diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index b5808133ec..d0104c45c3 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -91,8 +91,8 @@ $$ DECLARE tenant_cursor CURSOR FOR select tenant.id as tenant_id from tenant; - tenant_id_record varchar; - customer_id_record varchar; + tenant_id_record uuid; + customer_id_record uuid; tenant_ttl bigint; customer_ttl bigint; deleted_for_entities bigint; diff --git a/dao/src/main/resources/xss-policy.xml b/dao/src/main/resources/xss-policy.xml new file mode 100644 index 0000000000..6ea6660d2b --- /dev/null +++ b/dao/src/main/resources/xss-policy.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 124a846522..cb6de2fe89 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -25,7 +25,7 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.service.*ServiceNoSqlTest" + "org.thingsboard.server.dao.service.nosql.*ServiceNoSqlTest" }) public class NoSqlDaoServiceTestSuite { diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java index 4f26e4eb4e..c17c46c8a8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java @@ -24,7 +24,10 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.service.sql.*SqlTest" + "org.thingsboard.server.dao.service.sql.*SqlTest", + "org.thingsboard.server.dao.service.attributes.sql.*SqlTest", + "org.thingsboard.server.dao.service.event.sql.*SqlTest", + "org.thingsboard.server.dao.service.timeseries.sql.*SqlTest" }) public class SqlDaoServiceTestSuite { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index bd19ceb026..cfd232b330 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -48,6 +48,8 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; @@ -134,6 +136,12 @@ public abstract class AbstractServiceTest { @Autowired protected RuleChainService ruleChainService; + @Autowired + protected EdgeService edgeService; + + @Autowired + protected EdgeEventService edgeEventService; + @Autowired private ComponentDescriptorService componentDescriptorService; 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 36f4314d7a..98b1110ddc 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 @@ -25,7 +25,9 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -122,7 +124,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboard.setTenantId(tenantId); dashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); - tenant.setTitle("Test different tenant"); + tenant.setTitle("Test different tenant [dashboard]"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); @@ -327,4 +329,43 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } + @Test(expected = DataValidationException.class) + public void testAssignDashboardToNonExistentEdge() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(tenantId); + dashboard = dashboardService.saveDashboard(dashboard); + try { + dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), new EdgeId(Uuids.timeBased())); + } finally { + dashboardService.deleteDashboard(tenantId, dashboard.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testAssignDashboardToEdgeFromDifferentTenant() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(tenantId); + dashboard = dashboardService.saveDashboard(dashboard); + Tenant tenant = new Tenant(); + tenant.setTitle("Test different tenant [edge]"); + tenant = tenantService.saveTenant(tenant); + Edge edge = new Edge(); + edge.setTenantId(tenant.getId()); + edge.setType("default"); + edge.setName("Test different edge"); + edge.setType("default"); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + edge = edgeService.saveEdge(edge); + try { + dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); + } finally { + dashboardService.deleteDashboard(tenantId, dashboard.getId()); + tenantService.deleteTenant(tenant.getId()); + } + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java new file mode 100644 index 0000000000..a7a67a8f4b --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2021 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.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; + +public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { + + @Test + public void saveEdgeEvent() throws Exception { + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + EdgeEvent edgeEvent = generateEdgeEvent(null, edgeId, deviceId, EdgeEventActionType.ADDED); + EdgeEvent saved = edgeEventService.saveAsync(edgeEvent).get(); + Assert.assertEquals(saved.getTenantId(), edgeEvent.getTenantId()); + Assert.assertEquals(saved.getEdgeId(), edgeEvent.getEdgeId()); + Assert.assertEquals(saved.getEntityId(), edgeEvent.getEntityId()); + Assert.assertEquals(saved.getType(), edgeEvent.getType()); + Assert.assertEquals(saved.getAction(), edgeEvent.getAction()); + Assert.assertEquals(saved.getBody(), edgeEvent.getBody()); + } + + protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType edgeEventAction) throws IOException { + if (tenantId == null) { + tenantId = new TenantId(Uuids.timeBased()); + } + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setEntityId(entityId.getId()); + edgeEvent.setType(EdgeEventType.DEVICE); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setBody(readFromResource("TestJsonData.json")); + return edgeEvent; + } + + + @Test + public void findEdgeEventsByTimeDescOrder() throws Exception { + long timeBeforeStartTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC); + long startTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC); + long eventTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC); + long endTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); + long timeAfterEndTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); + + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); + saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent = saveEdgeEventWithProvidedTime(eventTime, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent2 = saveEdgeEventWithProvidedTime(eventTime + 1, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent3 = saveEdgeEventWithProvidedTime(eventTime + 2, edgeId, deviceId, tenantId); + saveEdgeEventWithProvidedTime(timeAfterEndTime, edgeId, deviceId, tenantId); + + TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); + PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); + + Assert.assertNotNull(edgeEvents.getData()); + Assert.assertTrue(edgeEvents.getData().size() == 2); + Assert.assertTrue(edgeEvents.getData().get(0).getUuidId().equals(savedEdgeEvent3.getUuidId())); + Assert.assertTrue(edgeEvents.getData().get(1).getUuidId().equals(savedEdgeEvent2.getUuidId())); + Assert.assertTrue(edgeEvents.hasNext()); + Assert.assertNotNull(pageLink.nextPageLink()); + + edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink.nextPageLink(), true); + + Assert.assertNotNull(edgeEvents.getData()); + Assert.assertTrue(edgeEvents.getData().size() == 1); + Assert.assertTrue(edgeEvents.getData().get(0).getUuidId().equals(savedEdgeEvent.getUuidId())); + Assert.assertFalse(edgeEvents.hasNext()); + } + + @Test + public void findEdgeEventsWithTsUpdateAndWithout() throws Exception { + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); + TimePageLink pageLink = new TimePageLink(1); + + EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED); + edgeEventService.saveAsync(edgeEventWithTsUpdate).get(); + + PageData allEdgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); + PageData edgeEventsWithoutTsUpdate = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false); + + Assert.assertNotNull(allEdgeEvents.getData()); + Assert.assertNotNull(edgeEventsWithoutTsUpdate.getData()); + Assert.assertEquals(1, allEdgeEvents.getData().size()); + Assert.assertEquals(allEdgeEvents.getData().get(0).getUuidId(), edgeEventWithTsUpdate.getUuidId()); + Assert.assertTrue(edgeEventsWithoutTsUpdate.getData().isEmpty()); + } + + private EdgeEvent saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { + EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); + edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); + return edgeEventService.saveAsync(edgeEvent).get(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..4725b52e6f --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -0,0 +1,598 @@ +/** + * Copyright © 2016-2021 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.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +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.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +public abstract class BaseEdgeServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveEdge() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(edge.getTenantId(), savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); + + savedEdge.setName("My new edge"); + + edgeService.saveEdge(savedEdge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); + + edgeService.deleteEdge(tenantId, savedEdge.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithEmptyName() { + Edge edge = new Edge(); + edge.setType("default"); + edge.setTenantId(tenantId); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithEmptyTenant() { + Edge edge = new Edge(); + edge.setName("My edge"); + edge.setType("default"); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithInvalidTenant() { + Edge edge = new Edge(); + edge.setName("My edge"); + edge.setType("default"); + edge.setTenantId(new TenantId(Uuids.timeBased())); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testAssignEdgeToNonExistentCustomer() { + Edge edge = constructEdge("My edge", "default"); + edge = edgeService.saveEdge(edge); + try { + edgeService.assignEdgeToCustomer(tenantId, edge.getId(), new CustomerId(Uuids.timeBased())); + } finally { + edgeService.deleteEdge(tenantId, edge.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testAssignEdgeToCustomerFromDifferentTenant() { + Edge edge = constructEdge("My edge", "default"); + edge = 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); + try { + edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customer.getId()); + } finally { + edgeService.deleteEdge(tenantId, edge.getId()); + tenantService.deleteTenant(tenant.getId()); + } + } + + @Test + public void testFindEdgeById() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNotNull(foundEdge); + Assert.assertEquals(savedEdge, foundEdge); + edgeService.deleteEdge(tenantId, savedEdge.getId()); + } + + @Test + public void testFindEdgeTypesByTenantId() throws Exception { + List edges = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + Edge edge = constructEdge("My edge B" + i, "typeB"); + edges.add(edgeService.saveEdge(edge)); + } + for (int i = 0; i < 7; i++) { + Edge edge = constructEdge("My edge C" + i, "typeC"); + edges.add(edgeService.saveEdge(edge)); + } + for (int i = 0; i < 9; i++) { + Edge edge = constructEdge("My edge A" + i, "typeA"); + edges.add(edgeService.saveEdge(edge)); + } + List edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId).get(); + Assert.assertNotNull(edgeTypes); + Assert.assertEquals(3, edgeTypes.size()); + Assert.assertEquals("typeA", edgeTypes.get(0).getType()); + Assert.assertEquals("typeB", edgeTypes.get(1).getType()); + Assert.assertEquals("typeC", edgeTypes.get(2).getType()); + } finally { + edges.forEach((edge) -> { + edgeService.deleteEdge(tenantId, edge.getId()); + }); + } + } + + @Test + public void testDeleteEdge() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNotNull(foundEdge); + edgeService.deleteEdge(tenantId, savedEdge.getId()); + foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNull(foundEdge); + } + + @Test + public void testFindEdgesByTenantId() { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + Edge edge = constructEdge(tenantId, "Edge " + i, "default"); + edges.add(edgeService.saveEdge(edge)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + + edgeService.deleteEdgesByTenantId(tenantId); + + pageLink = new PageLink(33); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindEdgesByTenantIdAndName() { + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle1.add(edgeService.saveEdge(edge)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle2.add(edgeService.saveEdge(edge)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindEdgesByTenantIdAndType() { + String title1 = "Edge title 1"; + String type1 = "typeA"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edgesType1.add(edgeService.saveEdge(edge)); + } + String title2 = "Edge title 2"; + String type2 = "typeB"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edgesType2.add(edgeService.saveEdge(edge)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0 , title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type1, pageLink); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type2, pageLink); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type1, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type2, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindEdgesByTenantIdAndCustomerId() { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 278; i++) { + Edge edge = constructEdge(tenantId, "Edge" + i, "default"); + edge = edgeService.saveEdge(edge); + edges.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + + edgeService.unassignCustomerEdges(tenantId, customerId); + + pageLink = new PageLink(33); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindEdgesByTenantIdCustomerIdAndName() { + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = edgeService.saveEdge(edge); + edgesTitle1.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = edgeService.saveEdge(edge); + edgesTitle2.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + customerService.deleteCustomer(tenantId, customerId); + } + + @Test + public void testFindEdgesByTenantIdCustomerIdAndType() { + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + String type1 = "typeC"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edge = edgeService.saveEdge(edge); + edgesType1.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + String title2 = "Edge title 2"; + String type2 = "typeD"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edge = edgeService.saveEdge(edge); + edgesType2.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4); + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4); + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4); + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + customerService.deleteCustomer(tenantId, customerId); + } + + private Edge constructEdge(String name, String type) { + return constructEdge(tenantId, name, type); + } + + private Edge constructEdge(TenantId tenantId, String name, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName(name); + edge.setType(type); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 6b7530c4c3..e16fc3dac7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.junit.After; import org.junit.Assert; @@ -29,8 +30,10 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -44,6 +47,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -214,7 +219,89 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Assert.assertEquals(0, count); } - + @Test + public void testCountEdgeEntitiesByQuery() throws InterruptedException { + List edges = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Edge edge = createEdge(i, "default"); + edges.add(edgeService.saveEdge(edge)); + } + + EdgeTypeFilter filter = new EdgeTypeFilter(); + filter.setEdgeType("default"); + filter.setEdgeNameFilter(""); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + filter.setEdgeType("unknown"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + + filter.setEdgeType("default"); + filter.setEdgeNameFilter("Edge1"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(11, count); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityType(EntityType.EDGE); + entityListFilter.setEntityList(edges.stream().map(Edge::getId).map(EdgeId::toString).collect(Collectors.toList())); + + countQuery = new EntityCountQuery(entityListFilter); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + edgeService.deleteEdgesByTenantId(tenantId); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + } + + @Test + public void testCountHierarchicalEntitiesByEdgeSearchQuery() throws InterruptedException { + for (int i = 0; i < 5; i++) { + Edge edge = createEdge(i, "type" + i); + edge = edgeService.saveEdge(edge); + //TO make sure devices have different created time + Thread.sleep(1); + + EntityRelation er = new EntityRelation(); + er.setFrom(tenantId); + er.setTo(edge.getId()); + er.setType("Manages"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + } + + EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + filter.setRelationType("Manages"); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(5, count); + + filter.setEdgeTypes(Arrays.asList("type0", "type1")); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(2, count); + } + + private Edge createEdge(int i, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName("Edge" + i); + edge.setType(type); + edge.setLabel("EdgeLabel" + i); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } + @Test public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException { List assets = new ArrayList<>(); 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 b0cb197c17..e7cd349e73 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 @@ -23,12 +23,14 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.RuleChainId; 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.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.exception.DataValidationException; @@ -143,7 +145,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(16); PageData pageData = null; do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChains.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -158,7 +160,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainsByTenantId(tenantId); pageLink = new PageLink(31); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -194,7 +196,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(19, 0, name1); PageData pageData = null; do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChainsName1.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -209,7 +211,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { List loadedRuleChainsName2 = new ArrayList<>(); pageLink = new PageLink(4, 0, name2); do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChainsName2.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -226,7 +228,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } pageLink = new PageLink(4, 0, name1); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -235,7 +237,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } pageLink = new PageLink(4, 0, name2); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -327,6 +329,44 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); } + @Test + public void testGetDefaultEdgeRuleChains() throws Exception { + RuleChainId ruleChainId = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 1"); + saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 2"); + List result = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId).get(); + Assert.assertEquals(2, result.size()); + + ruleChainService.unsetAutoAssignToEdgeRuleChain(tenantId, ruleChainId); + + result = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId).get(); + Assert.assertEquals(1, result.size()); + } + + @Test + public void setEdgeTemplateRootRuleChain() throws Exception { + RuleChainId ruleChainId1 = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 1"); + RuleChainId ruleChainId2 = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 2"); + + ruleChainService.setEdgeTemplateRootRuleChain(tenantId, ruleChainId1); + ruleChainService.setEdgeTemplateRootRuleChain(tenantId, ruleChainId2); + + RuleChain ruleChainById = ruleChainService.findRuleChainById(tenantId, ruleChainId1); + Assert.assertFalse(ruleChainById.isRoot()); + + ruleChainById = ruleChainService.findRuleChainById(tenantId, ruleChainId2); + Assert.assertTrue(ruleChainById.isRoot()); + } + + private RuleChainId saveRuleChainAndSetAutoAssignToEdge(String name) { + RuleChain edgeRuleChain = new RuleChain(); + edgeRuleChain.setTenantId(tenantId); + edgeRuleChain.setType(RuleChainType.EDGE); + edgeRuleChain.setName(name); + RuleChain savedEdgeRuleChain = ruleChainService.saveRuleChain(edgeRuleChain); + ruleChainService.setAutoAssignToEdgeRuleChain(tenantId, savedEdgeRuleChain.getId()); + return savedEdgeRuleChain.getId(); + } + private RuleChainMetaData createRuleChainMetadata() throws Exception { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); 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 new file mode 100644 index 0000000000..8463e722cb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 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.service; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import javax.validation.ConstraintValidatorContext; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; + +public class NoXssValidatorTest { + private static NoXssValidator validator; + + @BeforeAll + public static void beforeAll() { + validator = new NoXssValidator(); + validator.initialize(null); + } + + @ParameterizedTest + @ValueSource(strings = { + "aboba666", + "909090909", + "qwertyyyy", + "bambam", + "

Link!!!

1221", + "

Please log in to proceed

Username:

Password:



", + " ", + "123 bebe", + }) + public void testIsNotValid(String stringWithXss) { + boolean isValid = validator.isValid(stringWithXss, mock(ConstraintValidatorContext.class)); + assertFalse(isValid); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java new file mode 100644 index 0000000000..4e772917b5 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.service.sql; + +import org.thingsboard.server.dao.service.BaseEdgeEventServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeEventServiceSqlTest extends BaseEdgeEventServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java new file mode 100644 index 0000000000..f522103814 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.service.sql; + +import org.thingsboard.server.dao.service.BaseEdgeServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeServiceSqlTest extends BaseEdgeServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 8ef3e59fcc..b7872fc239 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -143,52 +143,52 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { public void testFindByQueryAscOrder() throws Exception { DeviceId deviceId = new DeviceId(Uuids.timeBased()); + saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); - saveEntries(deviceId, TS); List queries = new ArrayList<>(); queries.add(new BaseReadTsKvQuery(STRING_KEY, TS - 3, TS, 0, 1000, Aggregation.NONE, "ASC")); List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(3, entries.size()); - Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(0)); - Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1)); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(2)); + Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(0)); + Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1)); + Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2)); EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(3, entries.size()); - Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(0)); - Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1)); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(2)); + Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(0)); + Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1)); + Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2)); } @Test public void testFindByQueryDescOrder() throws Exception { DeviceId deviceId = new DeviceId(Uuids.timeBased()); + saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); - saveEntries(deviceId, TS); List queries = new ArrayList<>(); queries.add(new BaseReadTsKvQuery(STRING_KEY, TS - 3, TS, 0, 1000, Aggregation.NONE, "DESC")); List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(3, entries.size()); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); - Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1)); - Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(2)); + Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0)); + Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1)); + Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(2)); EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(3, entries.size()); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); - Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1)); - Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(2)); + Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0)); + Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1)); + Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(2)); } @Test diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 36d73a96ca..ad65b25bfb 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -36,6 +36,9 @@ caffeine.specs.tenantProfiles.maxSize=100000 caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 caffeine.specs.deviceProfiles.maxSize=100000 +caffeine.specs.edges.timeToLiveInMinutes=1440 +caffeine.specs.edges.maxSize=100000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 @@ -48,3 +51,5 @@ security.claim.duration=60000 database.ts_max_intervals=700 sql.remove_null_chars=true + +edges.enabled=true diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index a548ceec30..fd8ce8c86c 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -29,4 +29,6 @@ DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 333ce03fba..80ac5ee340 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -30,3 +30,5 @@ DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 211cebb2ad..17cf4b6575 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -24,8 +24,10 @@ DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS tb_schema_settings; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; -DROP TABLE IF EXISTS api_usage_state; \ No newline at end of file +DROP TABLE IF EXISTS api_usage_state; diff --git a/dao/src/test/resources/xss-policy.xml b/dao/src/test/resources/xss-policy.xml new file mode 100644 index 0000000000..6ea6660d2b --- /dev/null +++ b/dao/src/test/resources/xss-policy.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b71a5c097b..d2440329ed 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -42,6 +42,7 @@ services: image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}" ports: - "8080" + - "7070" logging: driver: "json-file" options: @@ -50,6 +51,7 @@ services: environment: TB_SERVICE_ID: tb-core1 TB_SERVICE_TYPE: tb-core + EDGES_ENABLED: "true" env_file: - tb-node.env volumes: @@ -66,6 +68,7 @@ services: image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}" ports: - "8080" + - "7070" logging: driver: "json-file" options: @@ -74,6 +77,7 @@ services: environment: TB_SERVICE_ID: tb-core2 TB_SERVICE_TYPE: tb-core + EDGES_ENABLED: "true" env_file: - tb-node.env volumes: @@ -230,7 +234,7 @@ services: haproxy: restart: always container_name: "${LOAD_BALANCER_NAME}" - image: xalauc/haproxy-certbot:1.7.9 + image: thingsboard/haproxy-certbot:1.3.0 volumes: - ./haproxy/config:/config - ./haproxy/letsencrypt:/etc/letsencrypt @@ -239,6 +243,7 @@ services: - "80:80" - "443:443" - "1883:1883" + - "7070:7070" - "9999:9999" cap_add: - NET_ADMIN @@ -246,6 +251,7 @@ services: HTTP_PORT: 80 HTTPS_PORT: 443 MQTT_PORT: 1883 + EDGES_RPC_PORT: 7070 FORCE_HTTPS_REDIRECT: "false" links: - tb-core1 diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index 5ff76cfdcd..50dcf36434 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -49,12 +49,23 @@ listen mqtt-in server tbMqtt1 tb-mqtt-transport1:1883 check inter 5s resolvers docker_resolver resolve-prefer ipv4 server tbMqtt2 tb-mqtt-transport2:1883 check inter 5s resolvers docker_resolver resolve-prefer ipv4 +listen edges-rpc-in + bind *:${EDGES_RPC_PORT} + mode tcp + option clitcpka # For TCP keep-alive + timeout client 3h + timeout server 3h + option tcplog + balance leastconn + server tbEdgesRpc1 tb-core1:7070 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + server tbEdgesRpc2 tb-core2:7070 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + frontend http-in - bind *:${HTTP_PORT} + bind *:${HTTP_PORT} alpn h2,http/1.1 option forwardfor - reqadd X-Forwarded-Proto:\ http + http-request add-header "X-Forwarded-Proto" "http" acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ @@ -69,11 +80,11 @@ frontend http-in default_backend tb-web-backend frontend https_in - bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM + bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM alpn h2,http/1.1 option forwardfor - reqadd X-Forwarded-Proto:\ https + http-request add-header "X-Forwarded-Proto" "https" acl transport_http_acl path_beg /api/v1/ acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ diff --git a/k8s/common/tb-coap-transport-configmap.yml b/k8s/common/tb-coap-transport-configmap.yml index f38ee7ce0e..0f0ea0a8e5 100644 --- a/k8s/common/tb-coap-transport-configmap.yml +++ b/k8s/common/tb-coap-transport-configmap.yml @@ -23,11 +23,11 @@ metadata: name: tb-coap-transport-config data: conf: | - export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-coap-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" - export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" - export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" - export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" + export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-coap-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" + export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}-heapdump.bin" + export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" + export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" + export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-coap-transport.out export LOADER_PATH=/usr/share/tb-coap-transport/conf logback: | diff --git a/k8s/common/tb-http-transport-configmap.yml b/k8s/common/tb-http-transport-configmap.yml index ffd4c6833c..582df2e5e8 100644 --- a/k8s/common/tb-http-transport-configmap.yml +++ b/k8s/common/tb-http-transport-configmap.yml @@ -23,11 +23,11 @@ metadata: name: tb-http-transport-config data: conf: | - export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-http-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" - export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" - export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" - export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" + export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-http-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" + export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}-heapdump.bin" + export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" + export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" + export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-http-transport.out export LOADER_PATH=/usr/share/tb-http-transport/conf logback: | diff --git a/k8s/common/tb-mqtt-transport-configmap.yml b/k8s/common/tb-mqtt-transport-configmap.yml index 14db242110..0d82938842 100644 --- a/k8s/common/tb-mqtt-transport-configmap.yml +++ b/k8s/common/tb-mqtt-transport-configmap.yml @@ -23,11 +23,11 @@ metadata: name: tb-mqtt-transport-config data: conf: | - export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" - export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" - export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" - export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" + export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" + export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-heapdump.bin" + export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" + export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" + export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-mqtt-transport.out export LOADER_PATH=/usr/share/tb-mqtt-transport/conf logback: | diff --git a/k8s/common/tb-node-configmap.yml b/k8s/common/tb-node-configmap.yml index fd47c9b785..3e796215ab 100644 --- a/k8s/common/tb-node-configmap.yml +++ b/k8s/common/tb-node-configmap.yml @@ -24,11 +24,11 @@ metadata: data: conf: | export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data" - export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" - export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" - export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" - export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" + export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" + export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}-heapdump.bin" + export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" + export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" + export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=thingsboard.out export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions logback: | diff --git a/pom.xml b/pom.xml index ca878ca801..ec1df8c7e6 100755 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ 0.7.0 2.2.0 4.12 + 5.7.1 1.7.7 1.2.3 3.3.3 @@ -116,6 +117,10 @@ 1.0.2TB 3.4.0 7.54.2 + 6.0.13.Final + 3.0.0 + 2.0.1.Final + 1.6.2 2.8.5 @@ -919,6 +924,11 @@ lwm2m ${project.version} + + org.thingsboard.common + edge-api + ${project.version} + org.thingsboard dao @@ -934,6 +944,11 @@ stats ${project.version} + + org.thingsboard.common + coap-server + ${project.version} + org.thingsboard tools @@ -1242,6 +1257,11 @@ test-jar test + + org.eclipse.californium + scandium + ${californium.version} + com.google.code.gson gson @@ -1346,6 +1366,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${jupiter.version} + test + org.dbunit dbunit @@ -1543,6 +1569,36 @@ + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + org.glassfish + javax.el + ${javax.el.version} + + + javax.validation + validation-api + ${javax.validation-api.version} + + + org.owasp.antisamy + antisamy + ${antisamy.version} + + + org.slf4j + * + + + com.github.spotbugs + spotbugs-annotations + + + org.snmp4j snmp4j 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 2528e5f36c..de7ba29141 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 @@ -63,6 +63,9 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -70,6 +73,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; @@ -105,6 +109,7 @@ import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.model.SecuritySettings; @@ -116,6 +121,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.io.Closeable; import java.io.IOException; import java.net.URI; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -697,22 +703,31 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public List getComponentDescriptorsByType(ComponentType componentType) { + return getComponentDescriptorsByType(componentType, RuleChainType.CORE); + } + + public List getComponentDescriptorsByType(ComponentType componentType, RuleChainType ruleChainType) { return restTemplate.exchange( - baseURL + "/api/components?componentType={componentType}", + baseURL + "/api/components/" + componentType.name() + "/?ruleChainType={ruleChainType}", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }, - componentType).getBody(); + ruleChainType).getBody(); } public List getComponentDescriptorsByTypes(List componentTypes) { + return getComponentDescriptorsByTypes(componentTypes, RuleChainType.CORE); + } + + public List getComponentDescriptorsByTypes(List componentTypes, RuleChainType ruleChainType) { return restTemplate.exchange( - baseURL + "/api/components?componentTypes={componentTypes}", + baseURL + "/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }, - listEnumToString(componentTypes)) + listEnumToString(componentTypes), + ruleChainType) .getBody(); } @@ -2410,6 +2425,403 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Edge saveEdge(Edge edge) { + return restTemplate.postForEntity(baseURL + "/api/edge", edge, Edge.class).getBody(); + } + + public void deleteEdge(EdgeId edgeId) { + restTemplate.delete(baseURL + "/api/edge/{edgeId}", edgeId.getId()); + } + + public Optional getEdgeById(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.getForEntity(baseURL + "/api/edge/{edgeId}", Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignEdgeToCustomer(CustomerId customerId, EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/edge/{edgeId}", null, Edge.class, customerId.getId(), edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignEdgeToPublicCustomer(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.postForEntity(baseURL + "/api/customer/public/edge/{edgeId}", null, Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional setRootRuleChain(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/{ruleChainId}/root", null, Edge.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdges(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edges?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional unassignEdgeFromCustomer(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.exchange(baseURL + "/api/customer/edge/{edgeId}", HttpMethod.DELETE, HttpEntity.EMPTY, Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignDeviceToEdge(EdgeId edgeId, DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/device/{deviceId}", null, Device.class, edgeId.getId(), deviceId.getId()); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDeviceFromEdge(EdgeId edgeId, DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/device/{deviceId}", HttpMethod.DELETE, HttpEntity.EMPTY, Device.class, edgeId.getId(), deviceId.getId()); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeDevices(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/devices?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignAssetToEdge(EdgeId edgeId, AssetId assetId) { + try { + ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/asset/{assetId}", null, Asset.class, edgeId.getId(), assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignAssetFromEdge(EdgeId edgeId, AssetId assetId) { + try { + ResponseEntity asset = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/asset/{assetId}", HttpMethod.DELETE, HttpEntity.EMPTY, Asset.class, edgeId.getId(), assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeAssets(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/assets?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignDashboardToEdge(EdgeId edgeId, DashboardId dashboardId) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/dashboard/{dashboardId}", null, Dashboard.class, edgeId.getId(), dashboardId.getId()); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDashboardFromEdge(EdgeId edgeId, DashboardId dashboardId) { + try { + ResponseEntity dashboard = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/dashboard/{dashboardId}", HttpMethod.DELETE, HttpEntity.EMPTY, Dashboard.class, edgeId.getId(), dashboardId.getId()); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeDashboards(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/dashboards?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignEntityViewToEdge(EdgeId edgeId, EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/entityView/{entityViewId}", null, EntityView.class, edgeId.getId(), entityViewId.getId()); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignEntityViewFromEdge(EdgeId edgeId, EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/entityView/{entityViewId}", + HttpMethod.DELETE, HttpEntity.EMPTY, EntityView.class, edgeId.getId(), entityViewId.getId()); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeEntityViews(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/entityViews?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignRuleChainToEdge(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/ruleChain/{ruleChainId}", null, RuleChain.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignRuleChainFromEdge(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/ruleChain/{ruleChainId}", HttpMethod.DELETE, HttpEntity.EMPTY, RuleChain.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeRuleChains(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/ruleChains?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional setAutoAssignToEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/autoAssignToEdge", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unsetAutoAssignToEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.exchange(baseURL + "/api/ruleChain/{ruleChainId}/autoAssignToEdge", HttpMethod.DELETE, HttpEntity.EMPTY, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAutoAssignToEdgeRuleChains() { + return restTemplate.exchange(baseURL + "/api/ruleChain/autoAssignToEdgeRuleChains", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional setRootEdgeTemplateRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/edgeTemplateRoot", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getTenantEdges(String type, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/edges?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional getTenantEdge(String edgeName) { + try { + ResponseEntity edge = restTemplate.getForEntity(baseURL + "/api/tenant/edges?edgeName={edgeName}", Edge.class, edgeName); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getCustomerEdges(CustomerId customerId, PageLink pageLink, String edgeType) { + Map params = new HashMap<>(); + params.put("customerId", customerId.getId().toString()); + params.put("type", edgeType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/edges?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public List getEdgesByIds(List edgeIds) { + return restTemplate.exchange(baseURL + "/api/edges?edgeIds={edgeIds}", + HttpMethod.GET, + HttpEntity.EMPTY, new ParameterizedTypeReference>() { + }, listIdsToString(edgeIds)).getBody(); + } + + public List findByQuery(EdgeSearchQuery query) { + return restTemplate.exchange( + baseURL + "/api/edges", + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getEdgeTypes() { + return restTemplate.exchange( + baseURL + "/api/edge/types", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public PageData getEdgeEvents(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/events?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public void syncEdge(EdgeId edgeId) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.toString()); + restTemplate.postForEntity(baseURL + "/api/edge/sync/{edgeId}", null, EdgeId.class, params); + } + @Deprecated public Optional getAttributes(String accessToken, String clientKeys, String sharedKeys) { Map params = new HashMap<>(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index d1e86c6d76..53b52491da 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.api; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -57,4 +58,6 @@ public @interface RuleNode { boolean customRelations() default false; + RuleChainType[] ruleChainTypes() default {RuleChainType.CORE, RuleChainType.EDGE}; + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 5f182c2c34..e4a71ee33d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -42,6 +43,8 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -152,6 +155,8 @@ public interface TbContext { // TODO: Does this changes the message? TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action); + void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); + /* * * METHODS TO PROCESS THE MESSAGES @@ -198,6 +203,10 @@ public interface TbContext { RuleEngineDeviceProfileCache getDeviceProfileCache(); + EdgeService getEdgeService(); + + EdgeEventService getEdgeEventService(); + ListeningExecutor getJsExecutor(); ListeningExecutor getMailExecutor(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java new file mode 100644 index 0000000000..3c7fc7a68c --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.rule.engine.api.msg; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; + +@Data +@AllArgsConstructor +public class DeviceEdgeUpdateMsg implements ToDeviceActorNotificationMsg { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final EdgeId edgeId; + + @Override + public MsgType getMsgType() { + return MsgType.DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 12bbdf450d..861e2a3f0d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.page.PageData; @@ -49,6 +50,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.user.UserService; @@ -238,6 +240,13 @@ public abstract class TbAbstractRelationActionNode dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new PageLink(200, 0, entitykey.getEntityName())); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java index a3e0c430aa..117e5d3225 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java @@ -22,7 +22,12 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; 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.id.*; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -67,6 +72,9 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode processEdge(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { + return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(entityContainer.getEntityId().getId())), edge -> { + if (edge != null) { + return processSave(ctx, sdId, relationType); + } else { + return Futures.immediateFuture(true); + } + }, ctx.getDbCallbackExecutor()); + } + private ListenableFuture processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> { if (device != null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index a3c1e9e730..2b2f0d76a4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -17,13 +17,17 @@ package org.thingsboard.rule.engine.action; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j @RuleNode( @@ -38,7 +42,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; configDirective = "tbActionNodeLogConfig", icon = "menu" ) - public class TbLogNode implements TbNode { private TbLogNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 2697655f67..ca98e104c5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -18,11 +18,14 @@ package org.thingsboard.rule.engine.action; import com.google.gson.Gson; import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; 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.queue.ServiceQueue; import org.thingsboard.server.common.msg.session.SessionMsgType; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index 384982427d..5cf1a3a7a5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -29,9 +29,14 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.guava.GuavaSession; @@ -62,7 +67,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " otherwise, the message will be routed via success chain.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeCustomTableConfig", - icon = "file_upload") + icon = "file_upload", + ruleChainTypes = RuleChainType.CORE) public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java index 30e6afd723..5820759107 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java @@ -21,7 +21,12 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; 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.id.*; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -60,6 +65,9 @@ public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNodeSupports next originator types:" + + "
DEVICE" + + "
ASSET" + + "
ENTITY_VIEW" + + "
DASHBOARD" + + "
TENANT" + + "
CUSTOMER" + + "
EDGE

" + + "As well node supports next message types:" + + "
POST_TELEMETRY_REQUEST" + + "
POST_ATTRIBUTES_REQUEST" + + "
ATTRIBUTES_UPDATED" + + "
ATTRIBUTES_DELETED" + + "
ALARM

" + + "Message will be routed via Failure route if node was not able to save cloud event to database or unsupported originator type/message type arrived. " + + "In case successful storage cloud event to database message will be routed via Success route.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbNodeEmptyConfig", + icon = "cloud_upload", + ruleChainTypes = RuleChainType.EDGE +) +public class TbMsgPushToCloudNode implements TbNode { + + private EmptyNodeConfiguration config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + // Implementation of this node is done on the Edge + } + + @Override + public void destroy() { + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java new file mode 100644 index 0000000000..79ced87bd7 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java @@ -0,0 +1,290 @@ +/** + * Copyright © 2016-2021 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.rule.engine.edge; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "push to edge", + configClazz = EmptyNodeConfiguration.class, + nodeDescription = "Push messages from cloud to edge", + nodeDetails = "Push messages from cloud to edge. " + + "Message originator must be assigned to particular edge or message originator is EDGE entity itself. " + + "This node used only on cloud instances to push messages from cloud to edge. " + + "Once message arrived into this node it’s going to be converted into edge event and saved to the database. " + + "Node doesn't push messages directly to edge, but stores event(s) in the edge queue. " + + "
Supports next originator types:" + + "
DEVICE" + + "
ASSET" + + "
ENTITY_VIEW" + + "
DASHBOARD" + + "
TENANT" + + "
CUSTOMER" + + "
EDGE

" + + "As well node supports next message types:" + + "
POST_TELEMETRY_REQUEST" + + "
POST_ATTRIBUTES_REQUEST" + + "
ATTRIBUTES_UPDATED" + + "
ATTRIBUTES_DELETED" + + "
ALARM

" + + "Message will be routed via Failure route if node was not able to save edge event to database or unsupported originator type/message type arrived. " + + "In case successful storage edge event to database message will be routed via Success route.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbNodeEmptyConfig", + icon = "cloud_download", + ruleChainTypes = RuleChainType.CORE +) +public class TbMsgPushToEdgeNode implements TbNode { + + private EmptyNodeConfiguration config; + + private static final ObjectMapper json = new ObjectMapper(); + + private static final String SCOPE = "scope"; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + if (DataConstants.EDGE_MSG_SOURCE.equalsIgnoreCase(msg.getMetaData().getValue(DataConstants.MSG_SOURCE_KEY))) { + log.debug("Ignoring msg from the cloud, msg [{}]", msg); + ctx.ack(msg); + return; + } + if (isSupportedOriginator(msg.getOriginator().getEntityType())) { + if (isSupportedMsgType(msg.getType())) { + processMsg(ctx, msg); + } else { + log.debug("Unsupported msg type {}", msg.getType()); + ctx.tellFailure(msg, new RuntimeException("Unsupported msg type '" + msg.getType() + "'")); + } + } else { + log.debug("Unsupported originator type {}", msg.getOriginator().getEntityType()); + ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + msg.getOriginator().getEntityType() + "'")); + } + } + + private void processMsg(TbContext ctx, TbMsg msg) { + if (EntityType.EDGE.equals(msg.getOriginator().getEntityType())) { + try { + EdgeEvent edgeEvent = buildEdgeEvent(msg, ctx); + if (edgeEvent != null) { + EdgeId edgeId = new EdgeId(msg.getOriginator().getId()); + edgeEvent.setEdgeId(edgeId); + ListenableFuture saveFuture = ctx.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(saveFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent event) { + ctx.tellNext(msg, SUCCESS); + ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); + } + + @Override + public void onFailure(Throwable th) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", ctx.getTenantId().getId(), edgeEvent, edgeId.getId(), th); + ctx.tellFailure(msg, th); + } + }, ctx.getDbCallbackExecutor()); + } + } catch (JsonProcessingException e) { + log.error("Failed to build edge event", e); + ctx.tellFailure(msg, e); + } + } else { + ListenableFuture> getEdgeIdsFuture = ctx.getEdgeService().findRelatedEdgeIdsByEntityId(ctx.getTenantId(), msg.getOriginator()); + Futures.addCallback(getEdgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + try { + EdgeEvent edgeEvent = buildEdgeEvent(msg, ctx); + if (edgeEvent == null) { + log.debug("Edge event type is null. Entity Type {}", msg.getOriginator().getEntityType()); + ctx.tellFailure(msg, new RuntimeException("Edge event type is null. Entity Type '" + msg.getOriginator().getEntityType() + "'")); + } else { + edgeEvent.setEdgeId(edgeId); + ListenableFuture saveFuture = ctx.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(saveFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent event) { + ctx.tellNext(msg, SUCCESS); + ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); + } + + @Override + public void onFailure(Throwable th) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", ctx.getTenantId().getId(), edgeEvent, edgeId.getId(), th); + ctx.tellFailure(msg, th); + } + }, ctx.getDbCallbackExecutor()); + } + } catch (JsonProcessingException e) { + log.error("Failed to build edge event", e); + ctx.tellFailure(msg, e); + } + } + } + } + + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + + }, ctx.getDbCallbackExecutor()); + } + } + + private EdgeEvent buildEdgeEvent(TbMsg msg, TbContext ctx) throws JsonProcessingException { + String msgType = msg.getType(); + if (DataConstants.ALARM.equals(msgType)) { + return buildEdgeEvent(ctx.getTenantId(), EdgeEventActionType.ADDED, getUUIDFromMsgData(msg), EdgeEventType.ALARM, null); + } else { + EdgeEventType edgeEventTypeByEntityType = EdgeUtils.getEdgeEventTypeByEntityType(msg.getOriginator().getEntityType()); + if (edgeEventTypeByEntityType == null) { + return null; + } + EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType); + Map entityBody = new HashMap<>(); + Map metadata = msg.getMetaData().getData(); + JsonNode dataJson = json.readTree(msg.getData()); + switch (actionType) { + case ATTRIBUTES_UPDATED: + case POST_ATTRIBUTES: + entityBody.put("kv", dataJson); + entityBody.put(SCOPE, getScope(metadata)); + break; + case ATTRIBUTES_DELETED: + List keys = json.treeToValue(dataJson.get("attributes"), List.class); + entityBody.put("keys", keys); + entityBody.put(SCOPE, getScope(metadata)); + break; + case TIMESERIES_UPDATED: + entityBody.put("data", dataJson); + entityBody.put("ts", metadata.get("ts")); + break; + } + return buildEdgeEvent(ctx.getTenantId(), actionType, msg.getOriginator().getId(), edgeEventTypeByEntityType, json.valueToTree(entityBody)); + } + } + + private String getScope(Map metadata) { + String scope = metadata.get(SCOPE); + if (StringUtils.isEmpty(scope)) { + // TODO: voba - move this to configuration of the node UI or some other place + scope = DataConstants.SERVER_SCOPE; + } + return scope; + } + + private EdgeEvent buildEdgeEvent(TenantId tenantId, EdgeEventActionType edgeEventAction, UUID entityId, EdgeEventType edgeEventType, JsonNode entityBody) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(edgeEventType); + edgeEvent.setBody(entityBody); + return edgeEvent; + } + + private UUID getUUIDFromMsgData(TbMsg msg) throws JsonProcessingException { + JsonNode data = json.readTree(msg.getData()).get("id"); + String id = json.treeToValue(data.get("id"), String.class); + return UUID.fromString(id); + } + + private EdgeEventActionType getEdgeEventActionTypeByMsgType(String msgType) { + EdgeEventActionType actionType; + if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType)) { + actionType = EdgeEventActionType.TIMESERIES_UPDATED; + } else if (DataConstants.ATTRIBUTES_UPDATED.equals(msgType)) { + actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; + } else if (SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType)) { + actionType = EdgeEventActionType.POST_ATTRIBUTES; + } else { + actionType = EdgeEventActionType.ATTRIBUTES_DELETED; + } + return actionType; + } + + private boolean isSupportedOriginator(EntityType entityType) { + switch (entityType) { + case DEVICE: + case ASSET: + case ENTITY_VIEW: + case DASHBOARD: + case TENANT: + case CUSTOMER: + case EDGE: + return true; + default: + return false; + } + } + + private boolean isSupportedMsgType(String msgType) { + return SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) + || SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType) + || DataConstants.ATTRIBUTES_UPDATED.equals(msgType) + || DataConstants.ATTRIBUTES_DELETED.equals(msgType) + || DataConstants.ALARM.equals(msgType); + } + + @Override + public void destroy() { + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 4193d83270..f4c2411a33 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -16,9 +16,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index fe362b4b93..f677b90158 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -17,8 +17,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 0c253e9c76..720cd76ae0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -16,8 +16,12 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index 1db60cebab..2148568925 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -16,8 +16,12 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index 1c22b357ad..398e2a3a6f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -16,8 +16,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -67,6 +72,9 @@ public class TbOriginatorTypeSwitchNode implements TbNode { case ENTITY_VIEW: relationType = "Entity View"; break; + case EDGE: + relationType = "Edge"; + break; case RULE_CHAIN: relationType = "Rule chain"; break; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index 17c49adfc7..8599a50ad8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -26,7 +26,11 @@ import com.google.protobuf.ByteString; import com.google.pubsub.v1.ProjectTopicName; import com.google.pubsub.v1.PubsubMessage; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -36,8 +40,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.concurrent.TimeUnit; -import static org.thingsboard.common.util.DonAsynchron.withCallback; - @Slf4j @RuleNode( type = ComponentType.EXTERNAL, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index f0f7756356..69c5a3ac74 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -15,31 +15,13 @@ */ package org.thingsboard.rule.engine.geo; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.locationtech.spatial4j.context.jts.JtsSpatialContext; -import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; -import org.locationtech.spatial4j.shape.Point; -import org.locationtech.spatial4j.shape.Shape; -import org.locationtech.spatial4j.shape.ShapeFactory; -import org.locationtech.spatial4j.shape.SpatialRelation; -import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; -import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.filter.TbMsgTypeFilterNodeConfiguration; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.Collections; -import java.util.List; - /** * Created by ashvayka on 19.01.18. */ diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index 8372ebdd10..c8415e3181 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -19,8 +19,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index f4e651288c..9797e3142e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -18,11 +18,11 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index e3303d074e..c4c8295823 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.ContactBased; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -118,6 +119,18 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { + if (edge != null) { + if (!edge.getCustomerId().isNullUid()) { + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), edge.getCustomerId()); + } else { + throw new RuntimeException("Edge with name '" + edge.getName() + "' is not assigned to Customer."); + } + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); default: throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 81e221683e..f988becb94 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -17,14 +17,13 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 438de99846..cdcc354611 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; /** * Created by ashvayka on 19.01.18. diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 0310b1bbe4..e9a382bb4d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -16,10 +16,12 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; - import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index f484a5551c..3c3fa8e21a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -47,7 +47,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 4c732e0d97..4f4dc7ab60 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -20,7 +20,6 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.util.StringUtils; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index ab0d9df7c2..928a403fc4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -33,10 +33,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; @@ -275,7 +272,7 @@ class AlarmRuleState { if (value == null) { return false; } - eval = eval && eval(data, value, filter.getPredicate()); + eval = eval && eval(data, value, filter.getPredicate(), filter); } return eval; } @@ -300,33 +297,33 @@ class AlarmRuleState { return value; } - private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate, AlarmConditionFilter filter) { switch (predicate.getType()) { case STRING: - return evalStrPredicate(data, value, (StringFilterPredicate) predicate); + return evalStrPredicate(data, value, (StringFilterPredicate) predicate, filter); case NUMERIC: - return evalNumPredicate(data, value, (NumericFilterPredicate) predicate); + return evalNumPredicate(data, value, (NumericFilterPredicate) predicate, filter); case BOOLEAN: - return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate); + return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate, filter); case COMPLEX: - return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate); + return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate, filter); default: return false; } } - private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) { + private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate, AlarmConditionFilter filter) { switch (predicate.getOperation()) { case OR: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (eval(data, ekv, kfp)) { + if (eval(data, ekv, kfp, filter)) { return true; } } return false; case AND: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (!eval(data, ekv, kfp)) { + if (!eval(data, ekv, kfp, filter)) { return false; } } @@ -336,12 +333,15 @@ class AlarmRuleState { } } - private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) { + private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate, AlarmConditionFilter filter) { Boolean val = getBoolValue(ekv); if (val == null) { return false; } - Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue); + Boolean predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getBoolValue); + if (predicateValue == null) { + return false; + } switch (predicate.getOperation()) { case EQUAL: return val.equals(predicateValue); @@ -352,12 +352,15 @@ class AlarmRuleState { } } - private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) { + private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate, AlarmConditionFilter filter) { Double val = getDblValue(ekv); if (val == null) { return false; } - Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue); + Double predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getDblValue); + if (predicateValue == null) { + return false; + } switch (predicate.getOperation()) { case NOT_EQUAL: return !val.equals(predicateValue); @@ -376,12 +379,15 @@ class AlarmRuleState { } } - private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) { + private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate, AlarmConditionFilter filter) { String val = getStrValue(ekv); if (val == null) { return false; } - String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue); + String predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getStrValue); + if (predicateValue == null) { + return false; + } if (predicate.isIgnoreCase()) { val = val.toLowerCase(); predicateValue = predicateValue.toLowerCase(); @@ -404,7 +410,7 @@ class AlarmRuleState { } } - private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, Function transformFunction) { + private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, AlarmConditionFilter filter, Function transformFunction) { EntityKeyValue ekv = getDynamicPredicateValue(data, value); if (ekv != null) { T result = transformFunction.apply(ekv); @@ -412,7 +418,11 @@ class AlarmRuleState { return result; } } - return value.getDefaultValue(); + if (filter.getKey().getType() != AlarmConditionKeyType.CONSTANT) { + return value.getDefaultValue(); + } else { + return null; + } } private EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue value) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index a8e32145d0..ceee362fc8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -31,8 +31,8 @@ import org.thingsboard.server.common.data.DataConstants; 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.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index f481c875a9..fea622bf30 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -16,17 +16,24 @@ package org.thingsboard.rule.engine.rabbitmq; import com.google.common.util.concurrent.ListenableFuture; -import com.rabbitmq.client.*; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.MessageProperties; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.charset.Charset; -import java.util.concurrent.ExecutionException; import static org.thingsboard.common.util.DonAsynchron.withCallback; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index 2f82ba64a1..408e27e50b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -17,14 +17,13 @@ package org.thingsboard.rule.engine.rpc; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; 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.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 42d45ca401..07952359a1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -16,13 +16,16 @@ package org.thingsboard.rule.engine.rpc; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -30,6 +33,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index d92fd24741..dc657cfce6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -18,13 +18,13 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java index 40c4bc4410..ba264ccc51 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java @@ -22,12 +22,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java index 9b35f81e93..0e85f473bb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java @@ -22,14 +22,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.concurrent.ExecutionException; - -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, diff --git a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java index 7afafcfb73..010a774e0e 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java +++ b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java @@ -26,7 +26,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling -@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) +@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.coapserver", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) public class ThingsboardCoapTransportApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index a9fe673b28..4d5bbf2c6f 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -46,6 +46,28 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 2a896c49f9..ec63dcae75 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -129,8 +129,7 @@ transport: timeout: "${LWM2M_TIMEOUT:120000}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" - request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" - request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" @@ -139,8 +138,7 @@ transport: # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" - # key_store_path_file: "${KEY_STORE_PATH_FILE:/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks}" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks}" key_store_path_file: "${KEY_STORE_PATH_FILE:}" key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" @@ -159,24 +157,26 @@ transport: # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" bootstrap: - enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" - id: "${LWM2M_SERVER_ID:111}" + enable: "${LWM2M_ENABLED_BS:true}" + id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" secure: bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: - alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BS:bootstrap}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 1749d89e4e..02c1c8649a 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -17,10 +17,9 @@ import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; import { AggregationType, calculateIntervalComparisonEndTime, - calculateIntervalEndTime, - calculateIntervalStartTime, + calculateIntervalEndTime, calculateIntervalStartEndTime, getCurrentTime, - getCurrentTimeForComparison, + getCurrentTimeForComparison, getTime, SubscriptionTimewindow } from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; @@ -36,8 +35,56 @@ interface AggData { aggValue: any; } -interface AggregationMap { - [key: string]: Map; +class AggDataMap { + rangeChanged = false; + private minTs = Number.MAX_SAFE_INTEGER; + private map = new Map(); + + set(ts: number, data: AggData) { + if (ts < this.minTs) { + this.rangeChanged = true; + this.minTs = ts; + } + this.map.set(ts, data); + } + + get(ts: number): AggData { + return this.map.get(ts); + } + + delete(ts: number) { + this.map.delete(ts); + } + + forEach(callback: (value: AggData, key: number, map: Map) => void, thisArg?: any) { + this.map.forEach(callback, thisArg); + } + + size(): number { + return this.map.size; + } +} + +class AggregationMap { + aggMap: {[key: string]: AggDataMap} = {}; + + detectRangeChanged(): boolean { + let changed = false; + for (const key of Object.keys(this.aggMap)) { + const aggDataMap = this.aggMap[key]; + if (aggDataMap.rangeChanged) { + changed = true; + aggDataMap.rangeChanged = false; + } + } + return changed; + } + + clearRangeChangedFlags() { + for (const key of Object.keys(this.aggMap)) { + this.aggMap[key].rangeChanged = false; + } + } } declare type AggFunction = (aggData: AggData, value?: any) => void; @@ -170,7 +217,7 @@ export class DataAggregator { updateIntervalScheduledTime = false; } if (update) { - this.aggregationMap = {}; + this.aggregationMap = new AggregationMap(); this.updateAggregatedData(data.data); } else { this.aggregationMap = this.processAggregatedData(data.data); @@ -178,12 +225,17 @@ export class DataAggregator { if (updateIntervalScheduledTime) { this.intervalScheduledTime = this.utils.currentPerfTime(); } + this.aggregationMap.clearRangeChangedFlags(); this.onInterval(history, detectChanges); } else { this.updateAggregatedData(data.data); if (history) { this.intervalScheduledTime = this.utils.currentPerfTime(); this.onInterval(history, detectChanges); + } else { + if (this.aggregationMap.detectRangeChanged()) { + this.onInterval(false, detectChanges, true); + } } } } @@ -192,18 +244,19 @@ export class DataAggregator { this.startTs = this.subsTw.startTs + this.subsTw.tsOffset; if (this.subsTw.quickInterval) { if (this.subsTw.timeForComparison === 'previousInterval') { + const startDate = getTime(this.subsTw.startTs, this.subsTw.timezone); const currentDate = getCurrentTime(this.subsTw.timezone); - this.endTs = calculateIntervalComparisonEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; + this.endTs = calculateIntervalComparisonEndTime(this.subsTw.quickInterval, startDate, currentDate) + this.subsTw.tsOffset; } else { - const currentDate = this.getCurrentTime(); - this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; + const startDate = getTime(this.subsTw.startTs, this.subsTw.timezone); + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, startDate, this.subsTw.timezone) + this.subsTw.tsOffset; } } else { this.endTs = this.startTs + this.subsTw.aggregation.timeWindow; } } - private onInterval(history?: boolean, detectChanges?: boolean) { + private onInterval(history?: boolean, detectChanges?: boolean, rangeChanged?: boolean) { const now = this.utils.currentPerfTime(); this.elapsed += now - this.intervalScheduledTime; this.intervalScheduledTime = now; @@ -211,14 +264,15 @@ export class DataAggregator { clearTimeout(this.intervalTimeoutHandle); this.intervalTimeoutHandle = null; } + const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout; if (!history) { const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval); - if (delta || !this.data) { + if (delta || !this.data || rangeChanged) { const tickTs = delta * this.subsTw.aggregation.interval; if (this.subsTw.quickInterval) { - const currentDate = this.getCurrentTime(); - this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; - this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; + const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone); + this.startTs = startEndTime[0] + this.subsTw.tsOffset; + this.endTs = startEndTime[1] + this.subsTw.tsOffset; } else { this.startTs += tickTs; this.endTs += tickTs; @@ -234,7 +288,7 @@ export class DataAggregator { this.updatedData = false; } if (!history) { - this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); + this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), intervalTimeout); } } @@ -242,18 +296,18 @@ export class DataAggregator { this.tsKeyNames.forEach((key) => { this.dataBuffer[key] = []; }); - for (const key of Object.keys(this.aggregationMap)) { - const aggKeyData = this.aggregationMap[key]; + for (const key of Object.keys(this.aggregationMap.aggMap)) { + const aggKeyData = this.aggregationMap.aggMap[key]; let keyData = this.dataBuffer[key]; aggKeyData.forEach((aggData, aggTimestamp) => { - if (aggTimestamp <= this.startTs) { + if (aggTimestamp < this.startTs) { if (this.subsTw.aggregation.stateData && (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; } aggKeyData.delete(aggTimestamp); this.updatedData = true; - } else if (aggTimestamp <= this.endTs) { + } else if (aggTimestamp < this.endTs) { const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; keyData.push(kvPair); } @@ -300,12 +354,12 @@ export class DataAggregator { private processAggregatedData(data: SubscriptionData): AggregationMap { const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; - const aggregationMap: AggregationMap = {}; + const aggregationMap = new AggregationMap(); for (const key of Object.keys(data)) { - let aggKeyData = aggregationMap[key]; + let aggKeyData = aggregationMap.aggMap[key]; if (!aggKeyData) { - aggKeyData = new Map(); - aggregationMap[key] = aggKeyData; + aggKeyData = new AggDataMap(); + aggregationMap.aggMap[key] = aggKeyData; } const keyData = data[key]; keyData.forEach((kvPair) => { @@ -326,10 +380,10 @@ export class DataAggregator { private updateAggregatedData(data: SubscriptionData) { const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; for (const key of Object.keys(data)) { - let aggKeyData = this.aggregationMap[key]; + let aggKeyData = this.aggregationMap.aggMap[key]; if (!aggKeyData) { - aggKeyData = new Map(); - this.aggregationMap[key] = aggKeyData; + aggKeyData = new AggDataMap(); + this.aggregationMap.aggMap[key] = aggKeyData; } const keyData = data[key]; keyData.forEach((kvPair) => { @@ -374,4 +428,3 @@ export class DataAggregator { } } - diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index c6f8121919..9c769f3777 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -277,7 +277,11 @@ export class EntityDataSubscription { dataAggregator.reset(newSubsTw); }); } - this.subscriber.setTsOffset(this.subsTw.tsOffset); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } targetCommand.query = this.dataCommand.query; this.subscriber.subscriptionCommands = [targetCommand]; } else { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 6663735bd5..b74a807729 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -37,12 +37,11 @@ import { } from '@app/shared/models/widget.models'; import { HttpErrorResponse } from '@angular/common/http'; import { - calculateIntervalEndTime, - calculateIntervalStartTime, + calculateIntervalStartEndTime, calculateTsOffset, ComparisonDuration, createSubscriptionTimewindow, createTimewindowForComparison, - getCurrentTime, isHistoryTypeTimewindow, + isHistoryTypeTimewindow, SubscriptionTimewindow, Timewindow, timewindowTypeChanged, toHistoryTimewindow, @@ -1106,11 +1105,9 @@ export class WidgetSubscription implements IWidgetSubscription { this.timeWindow.timezone = this.subscriptionTimewindow.timezone; if (this.subscriptionTimewindow.realtimeWindowMs) { if (this.subscriptionTimewindow.quickInterval) { - const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone); - this.timeWindow.maxTime = calculateIntervalEndTime( - this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; - this.timeWindow.minTime = calculateIntervalStartTime( - this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; + const startEndTime = calculateIntervalStartEndTime(this.subscriptionTimewindow.quickInterval, this.subscriptionTimewindow.timezone); + this.timeWindow.maxTime = startEndTime[1] + this.subscriptionTimewindow.tsOffset; + this.timeWindow.minTime = startEndTime[0] + this.subscriptionTimewindow.tsOffset; } else { this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff; this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index a7e8257a47..782702dc11 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -16,21 +16,20 @@ import { AuthUser, User } from '@shared/models/user.model'; -export interface AuthPayload { - authUser: AuthUser; - userDetails: User; +export interface SysParamsState { userTokenAccessEnabled: boolean; allowedDashboardIds: string[]; - forceFullscreen: boolean; + edgesSupportEnabled: boolean; } -export interface AuthState { - isAuthenticated: boolean; - isUserLoaded: boolean; +export interface AuthPayload extends SysParamsState { authUser: AuthUser; userDetails: User; - userTokenAccessEnabled: boolean; - allowedDashboardIds: string[]; forceFullscreen: boolean; +} + +export interface AuthState extends AuthPayload { + isAuthenticated: boolean; + isUserLoaded: boolean; lastPublicDashboardId: string; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 94dc95c6d2..9e3fe129f6 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -22,7 +22,8 @@ const emptyUserAuthState: AuthPayload = { userDetails: null, userTokenAccessEnabled: false, forceFullscreen: false, - allowedDashboardIds: [] + allowedDashboardIds: [], + edgesSupportEnabled: false }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 50160ad2d3..37802a08ed 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -31,7 +31,7 @@ import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; -import { AuthPayload, AuthState } from '@core/auth/auth.models'; +import { AuthPayload, AuthState, SysParamsState } from '@core/auth/auth.models'; import { TranslateService } from '@ngx-translate/core'; import { AuthUser } from '@shared/models/user.model'; import { TimeService } from '@core/services/time.service'; @@ -428,16 +428,24 @@ export class AuthService { } } - private loadSystemParams(authPayload: AuthPayload): Observable { - const sources: Array> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), - this.fetchAllowedDashboardIds(authPayload), - this.timeService.loadMaxDatapointsLimit()]; + public loadIsEdgesSupportEnabled(): Observable { + return this.http.get('/api/edges/enabled', defaultHttpOptions()); + } + + private loadSystemParams(authPayload: AuthPayload): Observable { + const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), + this.fetchAllowedDashboardIds(authPayload), + this.loadIsEdgesSupportEnabled(), + this.timeService.loadMaxDatapointsLimit()]; return forkJoin(sources) .pipe(map((data) => { - const userTokenAccessEnabled: boolean = data[0]; - const allowedDashboardIds: string[] = data[1]; - return {userTokenAccessEnabled, allowedDashboardIds}; - })); + const userTokenAccessEnabled: boolean = data[0] as boolean; + const allowedDashboardIds: string[] = data[1] as string[]; + const edgesSupportEnabled: boolean = data[2] as boolean; + return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled}; + }, catchError((err) => { + return of({}); + }))); } public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable { diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index 0163e69f6a..aa4bb161d1 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -89,4 +89,20 @@ export class AssetService { return this.http.get(`/api/tenant/assets?assetName=${assetName}`, defaultHttpOptionsFromConfig(config)); } + public assignAssetToEdge(edgeId: string, assetId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/asset/${assetId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignAssetFromEdge(edgeId: string, assetId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeAssets(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/assets${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/component-descriptor.service.ts b/ui-ngx/src/app/core/http/component-descriptor.service.ts index edca7a3543..0128131d7d 100644 --- a/ui-ngx/src/app/core/http/component-descriptor.service.ts +++ b/ui-ngx/src/app/core/http/component-descriptor.service.ts @@ -21,14 +21,15 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-uti import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { RuleNodeType } from '@shared/models/rule-node.models'; +import { RuleChainType } from '@shared/models/rule-chain.models'; @Injectable({ providedIn: 'root' }) export class ComponentDescriptorService { - private componentsByType: Map> = - new Map>(); + private componentsByTypeByRuleChainType: Map>> = + new Map>>(); private componentsByClazz: Map = new Map(); constructor( @@ -36,14 +37,17 @@ export class ComponentDescriptorService { ) { } - public getComponentDescriptorsByType(componentType: ComponentType, config?: RequestConfig): Observable> { - const existing = this.componentsByType.get(componentType); + public getComponentDescriptorsByType(componentType: ComponentType, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + if (!this.componentsByTypeByRuleChainType.get(ruleChainType)) { + this.componentsByTypeByRuleChainType.set(ruleChainType, new Map>()); + } + const existing = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentType); if (existing) { return of(existing); } else { - return this.http.get>(`/api/components/${componentType}`, defaultHttpOptionsFromConfig(config)).pipe( + return this.http.get>(`/api/components/${componentType}&ruleChainType=${ruleChainType}`, defaultHttpOptionsFromConfig(config)).pipe( map((componentDescriptors) => { - this.componentsByType.set(componentType, componentDescriptors); + this.componentsByTypeByRuleChainType.get(ruleChainType).set(componentType, componentDescriptors); componentDescriptors.forEach((componentDescriptor) => { this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); }); @@ -53,12 +57,14 @@ export class ComponentDescriptorService { } } - public getComponentDescriptorsByTypes(componentTypes: Array, - config?: RequestConfig): Observable> { + public getComponentDescriptorsByTypes(componentTypes: Array, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + if (!this.componentsByTypeByRuleChainType.get(ruleChainType)) { + this.componentsByTypeByRuleChainType.set(ruleChainType, new Map>()); + } let result: ComponentDescriptor[] = []; for (let i = componentTypes.length - 1; i >= 0; i--) { const componentType = componentTypes[i]; - const componentDescriptors = this.componentsByType.get(componentType); + const componentDescriptors = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentType); if (componentDescriptors) { result = result.concat(componentDescriptors); componentTypes.splice(i, 1); @@ -67,14 +73,14 @@ export class ComponentDescriptorService { if (!componentTypes.length) { return of(result); } else { - return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}`, + return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}&ruleChainType=${ruleChainType}`, defaultHttpOptionsFromConfig(config)).pipe( map((componentDescriptors) => { componentDescriptors.forEach((componentDescriptor) => { - let componentsList = this.componentsByType.get(componentDescriptor.type); + let componentsList = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentDescriptor.type); if (!componentsList) { componentsList = new Array(); - this.componentsByType.set(componentDescriptor.type, componentsList); + this.componentsByTypeByRuleChainType.get(ruleChainType).set(componentDescriptor.type, componentsList); } componentsList.push(componentDescriptor); this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 3f3e512752..bf7042e985 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -170,4 +170,22 @@ export class DashboardService { return this.stDiffObservable; } + public getEdgeDashboards(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/dashboards${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + + public assignDashboardToEdge(edgeId: string, dashboardId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/dashboard/${dashboardId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignDashboardFromEdge(edgeId: string, dashboardId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/dashboard/${dashboardId}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index f7e82f3a41..7e3183255a 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -22,7 +22,7 @@ import {Observable} from 'rxjs'; import {PageData} from '@shared/models/page/page-data'; import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models'; import {isDefinedAndNotNull, isEmptyStr} from '@core/utils'; -import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/profile-config.models'; +import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; import {SortOrder} from '@shared/models/page/sort-order'; @Injectable({ diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index d1b9ab877b..04cf633e53 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -155,4 +155,22 @@ export class DeviceService { return this.http.delete(`/api/customer/device/${deviceName}/claim`, defaultHttpOptionsFromConfig(config)); } + public assignDeviceToEdge(edgeId: string, deviceId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/device/${deviceId}`, + defaultHttpOptionsFromConfig(config)); + } + + public unassignDeviceFromEdge(edgeId: string, deviceId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/device/${deviceId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeDevices(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/devices${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + } diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts new file mode 100644 index 0000000000..7e4974aec4 --- /dev/null +++ b/ui-ngx/src/app/core/http/edge.service.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2021 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, TimePageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { EntitySubtype } from '@app/shared/models/entity-type.models'; +import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +@Injectable({ + providedIn: 'root' +}) +export class EdgeService { + + constructor( + private http: HttpClient + ) { } + + public getEdges(edgeIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/edges?edgeIds=${edgeIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdge(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeInfo(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/info/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveEdge(edge: Edge, config?: RequestConfig): Observable { + return this.http.post('/api/edge', edge, defaultHttpOptionsFromConfig(config)); + } + + public deleteEdge(edgeId: string, config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/edge/types', defaultHttpOptionsFromConfig(config)); + } + + public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public assignEdgeToCustomer(customerId: string, edgeId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/edge/${edgeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public unassignEdgeFromCustomer(edgeId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/edge/${edgeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public makeEdgePublic(edgeId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/edge/${edgeId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public getTenantEdgeInfos(pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/edgeInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: EdgeSearchQuery, config?: RequestConfig): Observable> { + return this.http.post>('/api/edges', query, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeEvents(entityId: EntityId, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${entityId.id}/events` + `${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public syncEdge(edgeId: string, config?: RequestConfig) { + return this.http.post(`/api/edge/sync/${edgeId}`, edgeId, defaultHttpOptionsFromConfig(config)); + } + + public findMissingToRelatedRuleChains(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/missingToRelatedRuleChains/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public findByName(edgeName: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index 4b4bec1a49..5e9b33145d 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -83,4 +83,21 @@ export class EntityViewService { return this.http.post>('/api/entityViews', query, defaultHttpOptionsFromConfig(config)); } + public assignEntityViewToEdge(edgeId: string, entityViewId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/entityView/${entityViewId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignEntityViewFromEdge(edgeId: string, entityViewId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/entityView/${entityViewId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeEntityViews(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/entityViews${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 0862e10a76..90275f1016 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -28,7 +28,7 @@ import { UserService } from './user.service'; import { DashboardService } from '@core/http/dashboard.service'; import { Direction } from '@shared/models/page/sort-order'; import { PageData } from '@shared/models/page/page-data'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; @@ -45,6 +45,7 @@ import { DataKey, Datasource, DatasourceType, KeyInfo } from '@app/shared/models import { UtilsService } from '@core/services/utils.service'; import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; import { + EdgeImportEntityData, EntitiesKeysByQuery, entityFields, EntityInfo, @@ -52,7 +53,7 @@ import { ImportEntityData } from '@shared/models/entity.models'; import { EntityRelationService } from '@core/http/entity-relation.service'; -import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; +import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull } from '@core/utils'; import { Asset } from '@shared/models/asset.models'; import { Device, DeviceCredentialsType } from '@shared/models/device.models'; import { AttributeService } from '@core/http/attribute.service'; @@ -74,6 +75,11 @@ import { StringOperation } from '@shared/models/query/query.models'; import { alarmFields } from '@shared/models/alarm.models'; +import { EdgeService } from "@core/http/edge.service"; +import { Edge, EdgeEventType } from '@shared/models/edge.models'; +import { RuleChainType } from "@shared/models/rule-chain.models"; +import { WidgetService } from "@core/http/widget.service"; +import { DeviceProfileService } from "@core/http/device-profile.service"; @Injectable({ providedIn: 'root' @@ -84,6 +90,7 @@ export class EntityService { private http: HttpClient, private store: Store, private deviceService: DeviceService, + private edgeService: EdgeService, private assetService: AssetService, private entityViewService: EntityViewService, private tenantService: TenantService, @@ -93,6 +100,8 @@ export class EntityService { private dashboardService: DashboardService, private entityRelationService: EntityRelationService, private attributeService: AttributeService, + private widgetService: WidgetService, + private deviceProfileService: DeviceProfileService, private utils: UtilsService ) { } @@ -107,6 +116,9 @@ export class EntityService { case EntityType.ASSET: observable = this.assetService.getAsset(entityId, config); break; + case EntityType.EDGE: + observable = this.edgeService.getEdge(entityId, config); + break; case EntityType.ENTITY_VIEW: observable = this.entityViewService.getEntityView(entityId, config); break; @@ -174,6 +186,9 @@ export class EntityService { case EntityType.ASSET: observable = this.assetService.getAssets(entityIds, config); break; + case EntityType.EDGE: + observable = this.edgeService.getEdges(entityIds, config); + break; case EntityType.ENTITY_VIEW: observable = this.getEntitiesByIdsObservable( (id) => this.entityViewService.getEntityView(id, config), @@ -282,6 +297,14 @@ export class EntityService { entitiesObservable = this.assetService.getTenantAssetInfos(pageLink, subType, config); } break; + case EntityType.EDGE: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.edgeService.getCustomerEdgeInfos(customerId, pageLink, subType, config); + } else { + entitiesObservable = this.edgeService.getTenantEdgeInfos(pageLink, subType, config); + } + break; case EntityType.ENTITY_VIEW: pageLink.sortOrder.property = 'name'; if (authUser.authority === Authority.CUSTOMER_USER) { @@ -309,7 +332,12 @@ export class EntityService { break; case EntityType.RULE_CHAIN: pageLink.sortOrder.property = 'name'; - entitiesObservable = this.ruleChainService.getRuleChains(pageLink, config); + if (RuleChainType[subType]) { + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, subType as RuleChainType, config); + } else { + // safe fallback to default core type + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, RuleChainType.CORE, config); + } break; case EntityType.DASHBOARD: pageLink.sortOrder.property = 'title'; @@ -489,6 +517,8 @@ export class EntityService { return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; case AliasFilterType.deviceType: return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.edgeType: + return entityTypes.indexOf(EntityType.EDGE) > -1 ? true : false; case AliasFilterType.entityViewType: return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; case AliasFilterType.relationsQuery: @@ -515,6 +545,8 @@ export class EntityService { return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; case AliasFilterType.deviceSearchQuery: return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.edgeSearchQuery: + return entityTypes.indexOf(EntityType.EDGE) > -1 ? true : false; case AliasFilterType.entityViewSearchQuery: return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; } @@ -550,14 +582,20 @@ export class EntityService { return entityType === EntityType.ASSET; case AliasFilterType.deviceType: return entityType === EntityType.DEVICE; + case AliasFilterType.edgeType: + return entityType === EntityType.EDGE; case AliasFilterType.entityViewType: return entityType === EntityType.ENTITY_VIEW; case AliasFilterType.relationsQuery: return true; + case AliasFilterType.apiUsageState: + return true; case AliasFilterType.assetSearchQuery: return entityType === EntityType.ASSET; case AliasFilterType.deviceSearchQuery: return entityType === EntityType.DEVICE; + case AliasFilterType.edgeSearchQuery: + return entityType === EntityType.EDGE; case AliasFilterType.entityViewSearchQuery: return entityType === EntityType.ENTITY_VIEW; } @@ -566,15 +604,18 @@ export class EntityService { public prepareAllowedEntityTypesList(allowedEntityTypes: Array, useAliasEntityTypes?: boolean): Array { - const authUser = getCurrentAuthUser(this.store); + const authState = getCurrentAuthState(this.store); const entityTypes: Array = []; - switch (authUser.authority) { + switch (authState.authUser.authority) { case Authority.SYS_ADMIN: entityTypes.push(EntityType.TENANT); break; case Authority.TENANT_ADMIN: entityTypes.push(EntityType.DEVICE); entityTypes.push(EntityType.ASSET); + if (authState.edgesSupportEnabled) { + entityTypes.push(EntityType.EDGE); + } entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.TENANT); entityTypes.push(EntityType.CUSTOMER); @@ -588,6 +629,9 @@ export class EntityService { case Authority.CUSTOMER_USER: entityTypes.push(EntityType.DEVICE); entityTypes.push(EntityType.ASSET); + if (authState.edgesSupportEnabled) { + entityTypes.push(EntityType.EDGE); + } entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.CUSTOMER); entityTypes.push(EntityType.USER); @@ -599,7 +643,7 @@ export class EntityService { } if (useAliasEntityTypes) { entityTypes.push(AliasEntityType.CURRENT_USER); - if (authUser.authority !== Authority.SYS_ADMIN) { + if (authState.authUser.authority !== Authority.SYS_ADMIN) { entityTypes.push(AliasEntityType.CURRENT_USER_OWNER); } } @@ -640,6 +684,7 @@ export class EntityService { entityFieldKeys.push(entityFields.type.keyName); break; case EntityType.DEVICE: + case EntityType.EDGE: case EntityType.ASSET: entityFieldKeys.push(entityFields.name.keyName); entityFieldKeys.push(entityFields.type.keyName); @@ -833,6 +878,9 @@ export class EntityService { case AliasFilterType.apiUsageState: result.entityFilter = deepClone(filter); return of(result); + case AliasFilterType.edgeType: + result.entityFilter = deepClone(filter); + return of(result); case AliasFilterType.relationsQuery: result.stateEntity = filter.rootStateEntity; let rootEntityType; @@ -854,6 +902,7 @@ export class EntityService { } case AliasFilterType.assetSearchQuery: case AliasFilterType.deviceSearchQuery: + case AliasFilterType.edgeSearchQuery: case AliasFilterType.entityViewSearchQuery: result.stateEntity = filter.rootStateEntity; if (result.stateEntity && stateEntityId) { @@ -889,6 +938,51 @@ export class EntityService { public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean, config?: RequestConfig): Observable { + const saveEntityObservable: Observable> = this.getSaveEntityObservable(entityType, entityData, config); + return saveEntityObservable.pipe( + mergeMap((entity) => { + return this.saveEntityData(entity.id, entityData, config).pipe( + map(() => { + return { create: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(err => { + if (update) { + let findEntityObservable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + findEntityObservable = this.deviceService.findByName(entityData.name, config); + break; + case EntityType.ASSET: + findEntityObservable = this.assetService.findByName(entityData.name, config); + break; + case EntityType.EDGE: + findEntityObservable = this.edgeService.findByName(entityData.name, config); + break; + } + return findEntityObservable.pipe( + mergeMap((entity) => { + const updateEntityTasks: Observable[] = this.getUpdateEntityTasks(entityType, entityData, entity, config); + return forkJoin(updateEntityTasks).pipe( + map(() => { + return { update: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + } else { + return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); + } + }) + ); + } + + private getSaveEntityObservable(entityType: EntityType, entityData: ImportEntityData, + config?: RequestConfig): Observable> { let saveEntityObservable: Observable>; switch (entityType) { case EntityType.DEVICE: @@ -919,70 +1013,92 @@ export class EntityService { }; saveEntityObservable = this.assetService.saveAsset(asset, config); break; + case EntityType.EDGE: + const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData; + const edge: Edge = { + name: edgeEntityData.name, + type: edgeEntityData.type, + label: edgeEntityData.label, + additionalInfo: { + description: edgeEntityData.description + }, + edgeLicenseKey: edgeEntityData.edgeLicenseKey, + cloudEndpoint: edgeEntityData.cloudEndpoint !== '' ? edgeEntityData.cloudEndpoint : window.location.origin, + routingKey: edgeEntityData.routingKey !== '' ? edgeEntityData.routingKey : guid(), + secret: edgeEntityData.secret !== '' ? edgeEntityData.secret : generateSecret(20) + }; + saveEntityObservable = this.edgeService.saveEdge(edge, config); + break; } - return saveEntityObservable.pipe( - mergeMap((entity) => { - return this.saveEntityData(entity.id, entityData, config).pipe( - map(() => { - return { create: { entity: 1 } } as ImportEntitiesResultInfo; - }), - catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - }), - catchError(err => { - if (update) { - let findEntityObservable: Observable>; - switch (entityType) { + return saveEntityObservable; + + } + + private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData, + entity: BaseData, config?: RequestConfig): Observable[] { + const tasks: Observable[] = []; + let result; + let additionalInfo; + switch (entityType) { + case EntityType.ASSET: + case EntityType.DEVICE: + result = entity as (Device | Asset); + additionalInfo = result.additionalInfo || {}; + if (result.label !== entityData.label || + result.type !== entityData.type || + additionalInfo.description !== entityData.description || + (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { + result.label = entityData.label; + result.type = entityData.type; + result.additionalInfo = additionalInfo; + result.additionalInfo.description = entityData.description; + if (result.id.entityType === EntityType.DEVICE) { + result.additionalInfo.gateway = entityData.gateway; + } + switch (result.id.entityType) { case EntityType.DEVICE: - findEntityObservable = this.deviceService.findByName(entityData.name, config); + tasks.push(this.deviceService.saveDevice(result, config)); break; case EntityType.ASSET: - findEntityObservable = this.assetService.findByName(entityData.name, config); + tasks.push(this.assetService.saveAsset(result, config)); break; } - return findEntityObservable.pipe( - mergeMap((entity) => { - const tasks: Observable[] = []; - const result: Device & Asset = entity as (Device | Asset); - const additionalInfo = result.additionalInfo || {}; - if (result.label !== entityData.label || - result.type !== entityData.type || - additionalInfo.description !== entityData.description || - (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { - result.label = entityData.label; - result.type = entityData.type; - result.additionalInfo = additionalInfo; - result.additionalInfo.description = entityData.description; - if (result.id.entityType === EntityType.DEVICE) { - result.additionalInfo.gateway = entityData.gateway; - } - if (result.id.entityType === EntityType.DEVICE && result.deviceProfileId) { - delete result.deviceProfileId; - } - switch (result.id.entityType) { - case EntityType.DEVICE: - tasks.push(this.deviceService.saveDevice(result, config)); - break; - case EntityType.ASSET: - tasks.push(this.assetService.saveAsset(result, config)); - break; - } - } - tasks.push(this.saveEntityData(entity.id, entityData, config)); - return forkJoin(tasks).pipe( - map(() => { - return { update: { entity: 1 } } as ImportEntitiesResultInfo; - }), - catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - }), - catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - } else { - return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); } - }) - ); + tasks.push(this.saveEntityData(entity.id, entityData, config)); + break; + case EntityType.EDGE: + result = entity as Edge; + additionalInfo = result.additionalInfo || {}; + const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData; + if (result.label !== edgeEntityData.label || + result.type !== edgeEntityData.type || + (edgeEntityData.cloudEndpoint !== '' && result.cloudEndpoint !== edgeEntityData.cloudEndpoint) || + (edgeEntityData.edgeLicenseKey !== '' && result.edgeLicenseKey !== edgeEntityData.edgeLicenseKey) || + (edgeEntityData.routingKey !== '' && result.routingKey !== edgeEntityData.routingKey) || + (edgeEntityData.secret !== '' && result.secret !== edgeEntityData.secret) || + additionalInfo.description !== edgeEntityData.description) { + result.label = edgeEntityData.label; + result.type = edgeEntityData.type; + result.additionalInfo = additionalInfo; + result.additionalInfo.description = edgeEntityData.description; + if (edgeEntityData.cloudEndpoint !== '') { + result.cloudEndpoint = edgeEntityData.cloudEndpoint; + } + if (edgeEntityData.edgeLicenseKey !== '') { + result.edgeLicenseKey = edgeEntityData.edgeLicenseKey; + } + if (edgeEntityData.routingKey !== '') { + result.routingKey = edgeEntityData.routingKey; + } + if (edgeEntityData.secret !== '') { + result.secret = edgeEntityData.secret; + } + tasks.push(this.edgeService.saveEdge(result, config)); + } + tasks.push(this.saveEntityData(entity.id, edgeEntityData, config)); + break; + } + return tasks; } public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable { @@ -1194,4 +1310,62 @@ export class EntityService { datasource.dataKeys.push(dataKey); }); } + + public getAssignedToEdgeEntitiesByType(edgeId: string, entityType: EntityType, pageLink: PageLink): Observable> { + let entitiesObservable: Observable>; + switch (entityType) { + case (EntityType.ASSET): + entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink); + break; + case (EntityType.DEVICE): + entitiesObservable = this.deviceService.getEdgeDevices(edgeId, pageLink); + break; + case (EntityType.ENTITY_VIEW): + entitiesObservable = this.entityViewService.getEdgeEntityViews(edgeId, pageLink); + break; + case (EntityType.DASHBOARD): + entitiesObservable = this.dashboardService.getEdgeDashboards(edgeId, pageLink); + break; + case (EntityType.RULE_CHAIN): + entitiesObservable = this.ruleChainService.getEdgeRuleChains(edgeId, pageLink); + break; + } + return entitiesObservable; + } + + public getEdgeEventContentByEntityType(entity: any): Observable { + let entityObservable: Observable; + const entityId: string = entity.entityId; + const entityType: any = entity.type; + switch (entityType) { + case EdgeEventType.DASHBOARD: + case EdgeEventType.ALARM: + case EdgeEventType.RULE_CHAIN: + case EdgeEventType.EDGE: + case EdgeEventType.USER: + case EdgeEventType.CUSTOMER: + case EdgeEventType.TENANT: + case EdgeEventType.ASSET: + case EdgeEventType.DEVICE: + case EdgeEventType.ENTITY_VIEW: + entityObservable = this.getEntity(entityType, entityId, { ignoreLoading: true, ignoreErrors: true }); + break; + case EdgeEventType.RULE_CHAIN_METADATA: + entityObservable = this.ruleChainService.getRuleChainMetadata(entityId); + break; + case EdgeEventType.WIDGET_TYPE: + entityObservable = this.widgetService.getWidgetTypeById(entityId); + break; + case EdgeEventType.WIDGETS_BUNDLE: + entityObservable = this.widgetService.getWidgetsBundle(entityId); + break; + case EdgeEventType.DEVICE_PROFILE: + entityObservable = this.deviceProfileService.getDeviceProfile(entityId); + break; + case EdgeEventType.RELATION: + entityObservable = of(entity.body); + break; + } + return entityObservable; + } } diff --git a/ui-ngx/src/app/core/http/event.service.ts b/ui-ngx/src/app/core/http/event.service.ts index 07d4c99aa7..9bf1b4e86f 100644 --- a/ui-ngx/src/app/core/http/event.service.ts +++ b/ui-ngx/src/app/core/http/event.service.ts @@ -38,4 +38,5 @@ export class EventService { `${pageLink.toQuery()}&tenantId=${tenantId}`, defaultHttpOptionsFromConfig(config)); } + } diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 69a5f2a5c5..74b69eb887 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -24,6 +24,7 @@ export * from './customer.service'; export * from './dashboard.service'; export * from './device.service'; export * from './entity.service'; +export * from './edge.service'; export * from './entity-relation.service'; export * from './entity-view.service'; export * from './event.service'; diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index ef10f10b2c..e65e7017e0 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -26,6 +26,7 @@ import { RuleChainConnectionInfo, RuleChainMetaData, ruleChainNodeComponent, + RuleChainType, ruleNodeTypeComponentTypes, unknownNodeComponent } from '@shared/models/rule-chain.models'; @@ -43,13 +44,15 @@ import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { deepClone, snakeCase } from '@core/utils'; import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; +import { Edge } from '@shared/models/edge.models'; @Injectable({ providedIn: 'root' }) export class RuleChainService { - private ruleNodeComponents: Array; + private ruleNodeComponentsMap: Map> = + new Map>(); private ruleNodeConfigFactories: {[directive: string]: ComponentFactory} = {}; constructor( @@ -59,8 +62,8 @@ export class RuleChainService { private translate: TranslateService ) { } - public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/ruleChains${pageLink.toQuery()}`, + public getRuleChains(pageLink: PageLink, type: RuleChainType = RuleChainType.CORE, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains${pageLink.toQuery()}&type=${type}`, defaultHttpOptionsFromConfig(config)); } @@ -116,18 +119,18 @@ export class RuleChainService { ); } - public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, config?: RequestConfig): + public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { - if (this.ruleNodeComponents) { - return of(this.ruleNodeComponents); + if (this.ruleNodeComponentsMap.get(ruleChainType)) { + return of(this.ruleNodeComponentsMap.get(ruleChainType)); } else { - return this.loadRuleNodeComponents(config).pipe( + return this.loadRuleNodeComponents(ruleChainType, config).pipe( mergeMap((components) => { return this.resolveRuleNodeComponentsUiResources(components, ruleNodeConfigResourcesModulesMap).pipe( map((ruleNodeComponents) => { - this.ruleNodeComponents = ruleNodeComponents; - this.ruleNodeComponents.push(ruleChainNodeComponent); - this.ruleNodeComponents.sort( + this.ruleNodeComponentsMap.set(ruleChainType, ruleNodeComponents); + this.ruleNodeComponentsMap.get(ruleChainType).push(ruleChainNodeComponent); + this.ruleNodeComponentsMap.get(ruleChainType).sort( (comp1, comp2) => { let result = comp1.type.toString().localeCompare(comp2.type.toString()); if (result === 0) { @@ -136,7 +139,7 @@ export class RuleChainService { return result; } ); - return this.ruleNodeComponents; + return this.ruleNodeComponentsMap.get(ruleChainType); }) ); }) @@ -148,8 +151,8 @@ export class RuleChainService { return this.ruleNodeConfigFactories[directive]; } - public getRuleNodeComponentByClazz(clazz: string): RuleNodeComponentDescriptor { - const found = this.ruleNodeComponents.filter((component) => component.clazz === clazz); + public getRuleNodeComponentByClazz(ruleChainType: RuleChainType = RuleChainType.CORE, clazz: string): RuleNodeComponentDescriptor { + const found = this.ruleNodeComponentsMap.get(ruleChainType).filter((component) => component.clazz === clazz); if (found && found.length) { return found[0]; } else { @@ -204,8 +207,8 @@ export class RuleChainService { } } - private loadRuleNodeComponents(config?: RequestConfig): Observable> { - return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, config).pipe( + private loadRuleNodeComponents(ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, ruleChainType, config).pipe( map((components) => { const ruleNodeComponents: RuleNodeComponentDescriptor[] = []; components.forEach((component) => { @@ -292,4 +295,38 @@ export class RuleChainService { ); } + public getEdgeRuleChains(edgeId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/ruleChains${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public assignRuleChainToEdge(edgeId: string, ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/ruleChain/${ruleChainId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignRuleChainFromEdge(edgeId: string, ruleChainId: string, config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); + } + + public setEdgeTemplateRootRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/ruleChain/${ruleChainId}/edgeTemplateRoot`, defaultHttpOptionsFromConfig(config)); + } + + public setAutoAssignToEdgeRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/ruleChain/${ruleChainId}/autoAssignToEdge`, defaultHttpOptionsFromConfig(config)); + } + + public unsetAutoAssignToEdgeRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/ruleChain/${ruleChainId}/autoAssignToEdge`, defaultHttpOptionsFromConfig(config)); + } + + public getAutoAssignToEdgeRuleChains(config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChain/autoAssignToEdgeRuleChains`, defaultHttpOptionsFromConfig(config)); + } + + public setEdgeRootRuleChain(edgeId: string, ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/${ruleChainId}/root`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index cabe78ff6e..6b0b9df4d0 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -292,6 +292,7 @@ export class ItemBufferService { y: origNode.y, name: origNode.name, componentClazz: origNode.component.clazz, + ruleChainType: origNode.ruleChainType }; if (origNode.targetRuleChainId) { node.targetRuleChainId = origNode.targetRuleChainId; @@ -330,7 +331,7 @@ export class ItemBufferService { const deltaX = x - ruleNodes.originX; const deltaY = y - ruleNodes.originY; for (const node of ruleNodes.nodes) { - const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz); + const component = this.ruleChainService.getRuleNodeComponentByClazz(node.ruleChainType, node.componentClazz); if (component) { let icon = ruleNodeTypeDescriptors.get(component.type).icon; let iconUrl: string = null; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 7783fdd2c3..99ae8934c6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -18,13 +18,13 @@ import { Injectable } from '@angular/core'; import { AuthService } from '../auth/auth.service'; import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; -import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; +import { selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { take } from 'rxjs/operators'; import { HomeSection, MenuSection } from '@core/services/menu.models'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Authority } from '@shared/models/authority.enum'; -import { AuthUser } from '@shared/models/user.model'; import { guid } from '@core/utils'; +import { AuthState } from '@core/auth/auth.models'; @Injectable({ providedIn: 'root' @@ -45,23 +45,23 @@ export class MenuService { } private buildMenu() { - this.store.pipe(select(selectAuthUser), take(1)).subscribe( - (authUser: AuthUser) => { - if (authUser) { + this.store.pipe(select(selectAuth), take(1)).subscribe( + (authState: AuthState) => { + if (authState.authUser) { let menuSections: Array; let homeSections: Array; - switch (authUser.authority) { + switch (authState.authUser.authority) { case Authority.SYS_ADMIN: - menuSections = this.buildSysAdminMenu(authUser); - homeSections = this.buildSysAdminHome(authUser); + menuSections = this.buildSysAdminMenu(authState); + homeSections = this.buildSysAdminHome(authState); break; case Authority.TENANT_ADMIN: - menuSections = this.buildTenantAdminMenu(authUser); - homeSections = this.buildTenantAdminHome(authUser); + menuSections = this.buildTenantAdminMenu(authState); + homeSections = this.buildTenantAdminHome(authState); break; case Authority.CUSTOMER_USER: - menuSections = this.buildCustomerUserMenu(authUser); - homeSections = this.buildCustomerUserHome(authUser); + menuSections = this.buildCustomerUserMenu(authState); + homeSections = this.buildCustomerUserHome(authState); break; } this.menuSections$.next(menuSections); @@ -71,7 +71,7 @@ export class MenuService { ); } - private buildSysAdminMenu(authUser: any): Array { + private buildSysAdminMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -159,7 +159,7 @@ export class MenuService { return sections; } - private buildSysAdminHome(authUser: any): Array { + private buildSysAdminHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( { @@ -232,7 +232,7 @@ export class MenuService { return homeSections; } - private buildTenantAdminMenu(authUser: any): Array { + private buildTenantAdminMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -285,7 +285,37 @@ export class MenuService { type: 'link', path: '/entityViews', icon: 'view_quilt' - }, + } + ); + if (authState.edgesSupportEnabled) { + sections.push( + { + id: guid(), + name: 'edge.management', + type: 'toggle', + path: '/edges', + height: '80px', + icon: 'router', + pages: [ + { + id: guid(), + name: 'edge.edge-instances', + type: 'link', + path: '/edges', + icon: 'router' + }, + { + id: guid(), + name: 'edge.rulechain-templates', + type: 'link', + path: '/edges/ruleChains', + icon: 'settings_ethernet' + } + ] + } + ); + } + sections.push( { id: guid(), name: 'widget.widget-library', @@ -333,7 +363,7 @@ export class MenuService { return sections; } - private buildTenantAdminHome(authUser: any): Array { + private buildTenantAdminHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( { @@ -391,7 +421,28 @@ export class MenuService { path: '/entityViews' } ] - }, + } + ); + if (authState.edgesSupportEnabled) { + homeSections.push( + { + name: 'edge.management', + places: [ + { + name: 'edge.edge-instances', + icon: 'router', + path: '/edges' + }, + { + name: 'edge.rulechain-templates', + icon: 'settings_ethernet', + path: '/edges/ruleChains' + } + ] + } + ); + } + homeSections.push( { name: 'resource.management', places: [ @@ -436,7 +487,7 @@ export class MenuService { return homeSections; } - private buildCustomerUserMenu(authUser: any): Array { + private buildCustomerUserMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -467,7 +518,20 @@ export class MenuService { type: 'link', path: '/entityViews', icon: 'view_quilt' - }, + } + ); + if (authState.edgesSupportEnabled) { + sections.push( + { + id: guid(), + name: 'edge.edge-instances', + type: 'link', + path: '/edges', + icon: 'router' + } + ); + } + sections.push( { id: guid(), name: 'dashboard.dashboards', @@ -479,8 +543,9 @@ export class MenuService { return sections; } - private buildCustomerUserHome(authUser: any): Array { - const homeSections: Array = [ + private buildCustomerUserHome(authState: AuthState): Array { + const homeSections: Array = []; + homeSections.push( { name: 'asset.view-assets', places: [ @@ -510,7 +575,23 @@ export class MenuService { path: '/entityViews' } ] - }, + } + ); + if (authState.edgesSupportEnabled) { + homeSections.push( + { + name: 'edge.management', + places: [ + { + name: 'edge.edge-instances', + icon: 'router', + path: '/edges' + } + ] + } + ); + } + homeSections.push( { name: 'dashboard.view-dashboards', places: [ @@ -521,7 +602,7 @@ export class MenuService { } ] } - ]; + ); return homeSections; } diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 2aa43f5f2b..08119e6ce8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -46,7 +46,7 @@ export interface EntityAliasesDialogData { widgets: Array; isSingleEntityAlias?: boolean; isSingleWidget?: boolean; - allowedEntityTypes?: Array; + allowedEntityTypes?: Array; disableAdd?: boolean; singleEntityAlias?: EntityAlias; customTitle?: string; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 45ff291954..7e760e3337 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -122,6 +122,7 @@ import { WidgetTypes } from '@home/components/dashboard-page/widget-types-panel.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; +import {AliasEntityType, EntityType} from "@shared/models/entity-type.models"; // @dynamic @Component({ @@ -175,6 +176,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC isToolbarOpenedAnimate = false; isRightLayoutOpened = false; + allowedEntityTypes: Array = null; + editingWidget: Widget = null; editingWidgetLayout: WidgetLayout = null; editingWidgetOriginal: Widget = null; @@ -351,6 +354,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }; this.window.parent.postMessage(JSON.stringify(message), '*'); } + + this.allowedEntityTypes = this.entityService.prepareAllowedEntityTypesList(null, true); } private reset() { @@ -555,7 +560,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC data: { entityAliases: deepClone(this.dashboard.configuration.entityAliases), widgets: this.dashboardUtils.getWidgetsArray(this.dashboard), - isSingleEntityAlias: false + isSingleEntityAlias: false, + allowedEntityTypes: this.allowedEntityTypes } }).afterClosed().subscribe((entityAliases) => { if (entityAliases) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index a5d50e9507..d893ec848f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -80,9 +80,9 @@ (mousedown)="widgetMouseDown($event, widget)" (click)="widgetClicked($event, widget)" (contextmenu)="openWidgetContextMenu($event, widget)"> -
-
- +
+
-
device.lwm2m-value - + {{ 'device.lwm2m-value-required' | translate }} - {{ 'device.lwm2m-value-json-error' | translate }} + {{ 'device.lwm2m-value-format-error' | translate }}
+ + + +
+ + +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts new file mode 100644 index 0000000000..834be7535c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts @@ -0,0 +1,90 @@ +/// +/// Copyright © 2016-2021 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, OnInit, SkipSelf} from "@angular/core"; +import {ErrorStateMatcher} from "@angular/material/core"; +import {DialogComponent} from "@shared/components/dialog.component"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {Router} from "@angular/router"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm} from "@angular/forms"; +import {TranslateService} from "@ngx-translate/core"; +import {JsonObject} from "@angular/compiler-cli/ngcc/src/packages/entry_point"; + +export interface Lwm2mAttributesDialogData { + readonly: boolean; + attributeLwm2m: JsonObject; + destName: string +} + +@Component({ + selector: 'tb-lwm2m-attributes-dialog', + templateUrl: './lwm2m-attributes-dialog.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [{provide: ErrorStateMatcher, useExisting: Lwm2mAttributesDialogComponent}], +}) +export class Lwm2mAttributesDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + + readonly = this.data.readonly; + + attributeLwm2m = this.data.attributeLwm2m; + + submitted = false; + + dirtyValue = false; + + attributeLwm2mDialogFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: Lwm2mAttributesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.attributeLwm2mDialogFormGroup = this.fb.group({ + keyFilters: [{}, []] + }); + this.attributeLwm2mDialogFormGroup.patchValue({keyFilters: this.attributeLwm2m}); + this.attributeLwm2mDialogFormGroup.get('keyFilters').valueChanges.subscribe((attributes) => { + this.attributeLwm2m = attributes; + }); + if (this.readonly) { + this.attributeLwm2mDialogFormGroup.disable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.attributeLwm2m); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html new file mode 100644 index 0000000000..e0a53c49c4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html @@ -0,0 +1,83 @@ + +
+
+ device-profile.lwm2m.attribute-lwm2m-destination + {{ titleText }} +
+
+ device-profile.lwm2m.attribute-lwm2m-name + device-profile.lwm2m.attribute-lwm2m-value +
+
+
+
+ + + + + {{ attributeLwm2mMap.get(attrKey[attributeLwm2m]) }} + + + + + + + + +
+ + {{ 'device-profile.lwm2m.key-name' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + + {{ 'device-profile.lwm2m.valid-attribute-lwm2m-key' | translate: {attrEnums: attrKeys} }} + + + {{ 'device-profile.lwm2m.valid-attribute-lwm2m-value' | translate: {attrEnums: attrKeys} }} + +
+ {{noDataText ? noDataText : 'device-profile.lwm2m.no-data'}} +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts new file mode 100644 index 0000000000..7d7af196f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2021 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, forwardRef, Input, OnInit} from "@angular/core"; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from "@angular/forms"; +import {Subscription} from "rxjs"; +import {PageComponent} from "@shared/components/page.component"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import { + ATTRIBUTE_KEYS, + ATTRIBUTE_LWM2M_ENUM, + ATTRIBUTE_LWM2M_MAP +} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models"; +import {isDefinedAndNotNull, isEmpty, isEmptyStr, isUndefinedOrNull} from "@core/utils"; + + +@Component({ + selector: 'tb-lwm2m-attributes-key-list', + templateUrl: './lwm2m-attributes-key-list.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true, + } + ] +}) +export class Lwm2mAttributesKeyListComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { + + attrKeys = ATTRIBUTE_KEYS; + + attrKey = ATTRIBUTE_LWM2M_ENUM; + + attributeLwm2mMap = ATTRIBUTE_LWM2M_MAP; + + @Input() disabled: boolean; + + @Input() titleText: string; + + @Input() noDataText: string; + + kvListFormGroup: FormGroup; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.kvListFormGroup = this.fb.group({}); + this.kvListFormGroup.addControl('keyVals', + this.fb.array([])); + } + + keyValsFormArray(): FormArray { + return this.kvListFormGroup.get('keyVals') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(keyValMap: { [key: string]: string }): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const keyValsControls: Array = []; + if (keyValMap) { + for (const property of Object.keys(keyValMap)) { + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { + keyValsControls.push(this.fb.group({ + key: [property, [Validators.required, this.attributeLwm2mKeyValidator]], + value: [keyValMap[property], this.attributeLwm2mValueValidator(property)] + })); + } + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { + // this.updateValidate(); + this.updateModel(); + }); + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + public removeKeyVal(index: number) { + (this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index); + } + + public addKeyVal() { + const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray; + keyValsFormArray.push(this.fb.group({ + key: ['', [Validators.required, this.attributeLwm2mKeyValidator]], + value: ['', []] + })); + } + + public validate(c?: FormControl) { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + let valid = true; + for (const entry of kvList) { + if (isUndefinedOrNull(entry.key) || isEmptyStr(entry.key) || !ATTRIBUTE_KEYS.includes(entry.key)) { + valid = false; + break; + } + if (entry.key !== 'ver' && isNaN(Number(entry.value))) { + valid = false; + break; + } + } + return (valid) ? null : { + keyVals: { + valid: false, + }, + }; + } + + private updateValidate (c?: FormControl) { + const kvList = this.kvListFormGroup.get('keyVals') as FormArray; + kvList.controls.forEach(fg => { + if (fg.get('key').value==='ver') { + fg.get('value').setValidators(null); + fg.get('value').setErrors(null); + } + else { + fg.get('value').setValidators(this.attributeLwm2mValueNumberValidator); + fg.get('value').setErrors(this.attributeLwm2mValueNumberValidator(fg.get('value'))); + } + }); + } + + private updateModel() { + this.updateValidate(); + if (this.validate() === null) { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + const keyValMap: { [key: string]: string | number } = {}; + kvList.forEach((entry) => { + if (isUndefinedOrNull(entry.value) || entry.key === 'ver' || isEmptyStr(entry.value.toString())) { + keyValMap[entry.key] = entry.value.toString(); + } else { + keyValMap[entry.key] = Number(entry.value) + } + }); + this.propagateChange(keyValMap); + } + else { + this.propagateChange(null); + } + } + + + private attributeLwm2mKeyValidator = (control: AbstractControl) => { + const key = control.value as string; + if (isDefinedAndNotNull(key) && !isEmpty(key)) { + if (!ATTRIBUTE_KEYS.includes(key)) { + return { + validAttributeKey: true + }; + } + } + return null; + } + + private attributeLwm2mValueNumberValidator = (control: AbstractControl) => { + if (isNaN(Number(control.value)) || Number(control.value) < 0) { + return { + 'validAttributeValue': true + }; + } + return null; + } + + private attributeLwm2mValueValidator = (property: string): Object [] => { + return property === 'ver'? [] : [this.attributeLwm2mValueNumberValidator]; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html new file mode 100644 index 0000000000..2011cd65f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html @@ -0,0 +1,32 @@ + +
+
+ {{attributeLwm2mToString()}} +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss new file mode 100644 index 0000000000..63dd0bdb3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 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-kv-map { + span.no-data-found { + position: relative; + display: flex; + height: 40px; + + &.disabled { + color: rgba(0, 0, 0, .38); + } + } + } +} + +:host ::ng-deep { + .mat-form-field-wrapper { + padding-bottom: 0; + } + .mat-form-field-infix { + border-top: 0; + } + .mat-form-field-underline { + bottom: 0; + } +} + +.vertical-padding { + padding: 0 0 10px 20px; +} + +.resource-name-lw-end{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align:end; + //width: 80px; + cursor: pointer; +} + +.resource-name-lw{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts new file mode 100644 index 0000000000..ca4b66ae36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts @@ -0,0 +1,159 @@ +/// +/// Copyright © 2016-2021 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, forwardRef, Inject, Input, Output} from "@angular/core"; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from "@angular/forms"; +import {coerceBooleanProperty} from "@angular/cdk/coercion"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {DeviceProfileService} from "@core/http/device-profile.service"; +import {WINDOW} from "@core/services/window.service"; +import {deepClone, isDefinedAndNotNull, isEmpty} from "@core/utils"; +import { + Lwm2mAttributesDialogComponent, + Lwm2mAttributesDialogData +} from "@home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component"; +import {MatDialog} from "@angular/material/dialog"; +import {TranslateService} from "@ngx-translate/core"; +import {ATTRIBUTE_LWM2M_LABEL} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models"; + + +@Component({ + selector: 'tb-profile-lwm2m-attributes', + templateUrl: './lwm2m-attributes.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesComponent), + multi: true + }] +}) +export class Lwm2mAttributesComponent implements ControlValueAccessor { + attributeLwm2mFormGroup: FormGroup; + attributeLwm2mLabel = ATTRIBUTE_LWM2M_LABEL; + + private requiredValue: boolean; + private dirty = false; + + @Input() + attributeLwm2m: {}; + + @Input() + isAttributeTelemetry: boolean; + + @Input() + destName: string; + + @Input() + disabled: boolean; + + @Output() + updateAttributeLwm2m = new EventEmitter(); + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private propagateChange = (v: any) => { + } + + constructor(private store: Store, + private dialog: MatDialog, + private fb: FormBuilder, + private deviceProfileService: DeviceProfileService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.attributeLwm2mFormGroup.disable({emitEvent: false}); + } else { + this.attributeLwm2mFormGroup.enable({emitEvent: false}); + } + } + + ngOnInit() { + this.attributeLwm2mFormGroup = this.fb.group({ + attributeLwm2m: [this.attributeLwm2m] + }); + } + + writeValue(value: {} | null): void {} + + attributeLwm2mToString = (): string => { + return this.isIconEditAdd () ? this.attributeLwm2mLabelToString() : this.translate.instant('device-profile.lwm2m.no-data'); + } + + private attributeLwm2mLabelToString = (): string => { + let label = JSON.stringify(this.attributeLwm2m); + label = deepClone(label.replace('{', '')); + label = deepClone(label.replace('}', '')); + this.attributeLwm2mLabel.forEach((value: string, key: string) => { + const dest = '\"' + key + '\"\:'; + label = deepClone(label.replace(dest, value)); + }); + return label; + } + + isDisableBtn (): boolean { + return this.disabled || this.isAttributeTelemetry ? !(isDefinedAndNotNull(this.attributeLwm2m) && + !isEmpty(this.attributeLwm2m) && this.disabled) : this.disabled; + } + + isIconView (): boolean { + return this.isAttributeTelemetry || this.disabled; + } + + isIconEditAdd (): boolean { + return isDefinedAndNotNull(this.attributeLwm2m) && !isEmpty(this.attributeLwm2m); + } + + isToolTipLabel (): string { + return this.disabled ? this.translate.instant('device-profile.lwm2m.attribute-lwm2m-tip') : + this.isAttributeTelemetry ? this.translate.instant('device-profile.lwm2m.attribute-lwm2m-disable-tip') : + this.translate.instant('device-profile.lwm2m.attribute-lwm2m-tip'); + } + + public editAttributesLwm2m = ($event: Event): void => { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(Lwm2mAttributesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + attributeLwm2m: this.disabled ? this.attributeLwm2m : deepClone(this.attributeLwm2m), + destName: this.destName + } + }).afterClosed().subscribe((result) => { + if (result) { + this.attributeLwm2m = result; + this.attributeLwm2mFormGroup.patchValue({attributeLwm2m: this.attributeLwm2m}); + this.updateAttributeLwm2m.next(this.attributeLwm2m); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index 0adc882f0f..b82da4bc06 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import {Component, forwardRef, Inject, Input} from '@angular/core'; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; import { DEFAULT_CLIENT_HOLD_OFF_TIME, DEFAULT_ID_SERVER, @@ -27,13 +27,13 @@ import { SECURITY_CONFIG_MODE, SECURITY_CONFIG_MODE_NAMES, ServerSecurityConfig -} from './profile-config.models'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { WINDOW } from '@core/services/window.service'; -import { pairwise, startWith } from 'rxjs/operators'; -import { DeviceProfileService } from '@core/http/device-profile.service'; +} from './lwm2m-profile-config.models'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {WINDOW} from '@core/services/window.service'; +import {pairwise, startWith} from 'rxjs/operators'; +import {DeviceProfileService} from '@core/http/device-profile.service'; @Component({ selector: 'tb-profile-lwm2m-device-config-server', diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 3d4030cc0e..84797bcb98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,7 +20,7 @@
-
+
{{ 'device-profile.lwm2m.client-only-observe-after-connect-label' | translate }} { } + private propagateChange = (v: any) => { + } constructor(private store: Store, private fb: FormBuilder, @@ -84,7 +86,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro lifetime: [null, Validators.required], defaultMinPeriod: [null, Validators.required], notifIfDisabled: [true, []], - binding: ['U', Validators.required], + binding: [DEFAULT_BINDING, Validators.required], bootstrapServer: [null, Validators.required], lwm2mServer: [null, Validators.required], }); @@ -118,7 +120,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - writeValue(value: ProfileConfigModels | null): void { + writeValue(value: Lwm2mProfileConfigModels | null): void { this.configurationValue = (Object.keys(value).length === 0) ? getDefaultProfileConfig() : value; this.lwm2mDeviceConfigFormGroup.patchValue({ configurationJson: this.configurationValue @@ -127,9 +129,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } private initWriteValue = (): void => { - const modelValue = {objectIds: null, objectsList: []} as ModelValue; + const modelValue = {objectIds: [], objectsList: []} as ModelValue; modelValue.objectIds = this.getObjectsFromJsonAllConfig(); - if (modelValue.objectIds !== null) { + if (modelValue.objectIds.length > 0) { const sortOrder = { property: 'id', direction: Direction.ASC @@ -165,13 +167,13 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro let configuration: DeviceProfileTransportConfiguration = null; if (this.lwm2mDeviceConfigFormGroup.valid && this.lwm2mDeviceProfileFormGroup.valid) { configuration = this.lwm2mDeviceConfigFormGroup.value.configurationJson; - configuration.type = DeviceTransportType.LWM2M; + // configuration.type = DeviceTransportType.LWM2M; } this.propagateChange(configuration); } private updateObserveAttrTelemetryObjectFormGroup = (objectsList: ObjectLwM2M[]): void => { - this.lwm2mDeviceProfileFormGroup.patchValue({ + this.lwm2mDeviceProfileFormGroup.patchValue({ observeAttrTelemetry: deepClone(this.getObserveAttrTelemetryObjects(objectsList)) }, {emitEvent: false}); @@ -198,22 +200,25 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => { const objectLwM2MS = deepClone(objectList); if (this.configurationValue.observeAttr && objectLwM2MS.length > 0) { - const observeArray = this.configurationValue.observeAttr.observe; const attributeArray = this.configurationValue.observeAttr.attribute; const telemetryArray = this.configurationValue.observeAttr.telemetry; let keyNameJson = this.configurationValue.observeAttr.keyName; if (this.includesNotZeroInstance(attributeArray, telemetryArray)) { this.addInstances(attributeArray, telemetryArray, objectLwM2MS); } - if (isDefinedAndNotNull(observeArray)) { - this.updateObserveAttrTelemetryObjects(observeArray, objectLwM2MS, OBSERVE); + if (isDefinedAndNotNull(this.configurationValue.observeAttr.observe) && + this.configurationValue.observeAttr.observe.length > 0) { + this.updateObserveAttrTelemetryObjects(this.configurationValue.observeAttr.observe, objectLwM2MS, OBSERVE); } - if (isDefinedAndNotNull(attributeArray)) { + if (isDefinedAndNotNull(attributeArray) && attributeArray.length > 0) { this.updateObserveAttrTelemetryObjects(attributeArray, objectLwM2MS, ATTRIBUTE); } - if (isDefinedAndNotNull(telemetryArray)) { + if (isDefinedAndNotNull(telemetryArray) && telemetryArray.length > 0) { this.updateObserveAttrTelemetryObjects(telemetryArray, objectLwM2MS, TELEMETRY); } + if (isDefinedAndNotNull(this.configurationValue.observeAttr.attributeLwm2m)) { + this.updateAttributeLwm2m(objectLwM2MS); + } if (isDefinedAndNotNull(keyNameJson)) { this.configurationValue.observeAttr.keyName = this.validateKeyNameObjects(keyNameJson, attributeArray, telemetryArray); this.upDateJsonAllConfig(); @@ -256,45 +261,72 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro [nameParameter] = true; } }); + } + private updateAttributeLwm2m = (objectLwM2MS: ObjectLwM2M[]): void => { + Object.keys(this.configurationValue.observeAttr.attributeLwm2m).forEach(key => { + const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); + let objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId); + if (objectLwM2M && instanceId) { + let instance = objectLwM2M.instances.find(instance => instance.id === +instanceId) + if (instance && resourceId) { + instance.resources.find(resource => resource.id === +resourceId) + .attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } else if (instance) { + instance.attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } + } else if (objectLwM2M) { + objectLwM2M.attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } + }); } - private updateKeyNameObjects = (clientObserveAttrTelemetry: ObjectLwM2M[]): void => { + private updateKeyNameObjects = (objectLwM2MS: ObjectLwM2M[]): void => { Object.keys(this.configurationValue.observeAttr.keyName).forEach(key => { const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); - const objectLwM2M = clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) + const objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) if (objectLwM2M) { objectLwM2M.instances.find(instance => instance.id === +instanceId) - .resources.find(resource => resource.id === +resourceId) - .keyName = this.configurationValue.observeAttr.keyName[key]; - }}); + .resources.find(resource => resource.id === +resourceId) + .keyName = this.configurationValue.observeAttr.keyName[key]; + } + }); } private validateKeyNameObjects = (nameJson: JsonObject, attributeArray: JsonArray, telemetryArray: JsonArray): {} => { const keyName = JSON.parse(JSON.stringify(nameJson)); let keyNameValidate = {}; - const keyAttrTelemetry = attributeArray.concat(telemetryArray) ; + const keyAttrTelemetry = attributeArray.concat(telemetryArray); Object.keys(keyName).forEach(key => { if (keyAttrTelemetry.includes(key)) { keyNameValidate[key] = keyName[key]; } }); - return keyNameValidate; + return keyNameValidate; } private updateObserveAttrTelemetryFromGroupToJson = (val: ObjectLwM2M[]): void => { const observeArray: Array = []; const attributeArray: Array = []; const telemetryArray: Array = []; + const attributeLwm2m: any = {}; const keyNameNew = {}; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { + if (isDefinedAndNotNull(obj.attributeLwm2m) && !isEmpty(obj.attributeLwm2m)) { + const pathObject = `/${obj.keyId}`; + attributeLwm2m[pathObject] = obj.attributeLwm2m; + } if (obj.hasOwnProperty(INSTANCES) && Array.isArray(obj.instances)) { obj.instances.forEach(instance => { + if (isDefinedAndNotNull(instance.attributeLwm2m) && !isEmpty(instance.attributeLwm2m)) { + const pathInstance = `/${obj.keyId}/${instance.id}`; + attributeLwm2m[pathInstance] = instance.attributeLwm2m; + } if (instance.hasOwnProperty(RESOURCES) && Array.isArray(instance.resources)) { instance.resources.forEach(resource => { if (resource.attribute || resource.telemetry) { - let pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; + const pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; if (resource.observe) { observeArray.push(pathRes); } @@ -305,6 +337,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro telemetryArray.push(pathRes); } keyNameNew[pathRes] = resource.keyName; + if (isDefinedAndNotNull(resource.attributeLwm2m) && !isEmpty(resource.attributeLwm2m)) { + attributeLwm2m[pathRes] = resource.attributeLwm2m; + } } }) } @@ -316,13 +351,14 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observe: observeArray, attribute: attributeArray, telemetry: telemetryArray, - keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew) + keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), + attributeLwm2m: attributeLwm2m }; } else { this.configurationValue.observeAttr.observe = observeArray; this.configurationValue.observeAttr.attribute = attributeArray; this.configurationValue.observeAttr.telemetry = telemetryArray; - this.configurationValue.observeAttr.keyName = this.sortObjectKeyPathJson(KEY_NAME, keyNameNew); + this.configurationValue.observeAttr.attributeLwm2m = attributeLwm2m; } } @@ -368,7 +404,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro }); } } - return (objectsIds.size > 0) ? Array.from(objectsIds) : null; + return (objectsIds.size > 0) ? Array.from(objectsIds) : []; } private upDateJsonAllConfig = (): void => { @@ -392,6 +428,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.keyId); this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.keyId); this.removeKeyNameFromJson(value.keyId); + this.removeAttributeLwm2mFromJson(value.keyId); this.updateObserveAttrTelemetryObjectFormGroup(objectsOld); this.upDateJsonAllConfig(); } @@ -413,4 +450,14 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } }); } + + private removeAttributeLwm2mFromJson = (keyId: string): void => { + debugger + const keyNameJson = this.configurationValue.observeAttr.attributeLwm2m; + Object.keys(keyNameJson).forEach(key => { + if (key.startsWith(`/${keyId}`)) { + delete keyNameJson[key]; + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html similarity index 95% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html index b37abd2c79..50f49a8ceb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html @@ -17,7 +17,7 @@ -->
- {{data.objectName}}    (object [{{data.objectKeyId}}]) + {{data.objectName}} (object <{{data.objectKeyId}}>) @@ -45,22 +55,10 @@
-
- Instance [{{instances.get('id').value}}] +
+ {{'device-profile.lwm2m.instance-label' | translate}} <{{instances.get('id').value}}>
-
- - -
-
+
-
+
+ + +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss similarity index 65% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss index 7dc73ee405..30ce849c83 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss @@ -13,14 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.vertical-padding { - padding: 0 0 10px 20px; -} - -.left-padding { - padding-left: 5px; -} - .tb-panel-title-height { user-select: none; min-height: 32px; @@ -28,21 +20,5 @@ .checkbox-padding { padding-left: 22px; -} - -.label-resource { - /*padding: 4px;*/ - color: #002699; - /*text-transform: uppercase;*/ - background: rgba(220, 220, 220, .35); - border-radius: 20px; - width: inherit; - /*margin: 10px 0;*/ - overflow: hidden; - font-size: 15px; - text-overflow: ellipsis; - white-space: nowrap; - opacity: .8; text-align:center; } - diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts index 814876dab8..c38c413e98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts @@ -14,7 +14,6 @@ /// limitations under the License. /// - import {Component, forwardRef, Input} from '@angular/core'; import { AbstractControl, @@ -28,19 +27,29 @@ import { import {Store} from '@ngrx/store'; import {AppState} from '@core/core.state'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {CLIENT_LWM2M, Instance, INSTANCES, ObjectLwM2M, ResourceLwM2M, RESOURCES} from './profile-config.models'; +import { + ATTRIBUTE, + ATTRIBUTE_LWM2M, + CLIENT_LWM2M, + Instance, + INSTANCES, + ObjectLwM2M, + ResourceLwM2M, + RESOURCES, + TELEMETRY +} from './lwm2m-profile-config.models'; import {deepClone, isDefinedAndNotNull, isEqual, isUndefined} from '@core/utils'; import {MatDialog} from '@angular/material/dialog'; import {TranslateService} from '@ngx-translate/core'; import { - Lwm2mObjectAddInstancesComponent, - Lwm2mObjectAddInstancesData -} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances.component'; + Lwm2mObjectAddInstancesData, + Lwm2mObjectAddInstancesDialogComponent +} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component'; @Component({ selector: 'tb-profile-lwm2m-observe-attr-telemetry', templateUrl: './lwm2m-observe-attr-telemetry.component.html', - styleUrls: ['./lwm2m-observe-attr-telemetry.component.css'], + styleUrls: [ './lwm2m-observe-attr-telemetry.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -56,6 +65,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor valuePrev = null as any; observeAttrTelemetryFormGroup: FormGroup; + resources = RESOURCES; get required(): boolean { return this.requiredValue; @@ -143,6 +153,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor name: objectLwM2M.name, multiple: objectLwM2M.multiple, mandatory: objectLwM2M.mandatory, + attributeLwm2m: objectLwM2M.attributeLwm2m, instances: this.createInstanceLwM2M(objectLwM2M.instances) }); })); @@ -152,6 +163,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor return this.fb.array(instancesLwM2M.map((instanceLwM2M) => { return this.fb.group({ id: instanceLwM2M.id, + attributeLwm2m: {value: instanceLwM2M.attributeLwm2m, disabled: this.disabled}, resources: {value: instanceLwM2M.resources, disabled: this.disabled} }); })); @@ -222,7 +234,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor $event.stopPropagation(); $event.preventDefault(); } - this.dialog.open(Lwm2mObjectAddInstancesComponent, { + this.dialog.open(Lwm2mObjectAddInstancesDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -240,6 +252,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor } private updateInstancesIds = (data: Lwm2mObjectAddInstancesData): void => { + debugger const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls .find(e => e.value.keyId === data.objectKeyId) as FormGroup; const instancesArray = objectLwM2MFormGroup.value.instances as Instance []; @@ -249,6 +262,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor r.attribute = false; r.telemetry = false; r.observe = false; + r.keyName = {}; + r.attributeLwm2m = {}; }); const valueOld = this.instancesToSetId(instancesArray); if (!isEqual(valueOld, data.instancesIds)) { @@ -286,6 +301,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor private pushInstance = (instancesFormArray: FormArray, x: number, instanceNew: Instance): void => { instancesFormArray.push(this.fb.group({ id: x, + attributeLwm2m: instanceNew.attributeLwm2m, resources: {value: instanceNew.resources, disabled: this.disabled} })); } @@ -297,4 +313,54 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor private instancesToSetId = (instances: Instance[]): Set => { return new Set(instances.map(x => x.id)); } + + getNameObjectLwm2m = (objectName: string, idVerObj: string): string => { + return objectName + ' <' + idVerObj + '>'; + } + getNameInstanceLwm2m = (instance: Instance, idVerObj: string): string => { + return ' instance <' + idVerObj + '/' + instance.id +'>'; + } + + updateAttributeLwm2mObject = (event: Event, objectKeyId: number): void => { + const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls + .find(e => e.value.keyId === objectKeyId) as FormGroup; + objectLwM2MFormGroup.patchValue({attributeLwm2m: event}); + } + + updateAttributeLwm2mInstance = (event: Event, indexInstance: number, objectKeyId: number): void => { + + const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls + .find(e => e.value.keyId === objectKeyId) as FormGroup; + const instancesFormArray = objectLwM2MFormGroup.get(INSTANCES) as FormArray; + instancesFormArray.at(indexInstance).patchValue({attributeLwm2m: event}); + } + + disableObserveInstance = (instance: AbstractControl): boolean => { + const checkedAttrTelemetry = this.observeInstance(instance); + if (checkedAttrTelemetry) { + instance.get(ATTRIBUTE_LWM2M).patchValue(null); + } + return checkedAttrTelemetry; + } + + disableObserveObject = (index: number): boolean => { + const object = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).at(index) as FormGroup; + const instances = object.controls.instances as FormArray; + const checkedAttrTelemetry = instances.controls.filter(instance => !this.disableObserveInstance(instance)); + if (checkedAttrTelemetry.length === 0) { + object.controls.attributeLwm2m.patchValue(null); + } + return checkedAttrTelemetry.length === 0; + } + + + observeInstance = (instance: AbstractControl): boolean => { + const resources = instance.get(RESOURCES).value as ResourceLwM2M[]; + if (isDefinedAndNotNull(resources)) { + const checkedAttribute = resources.filter(resource => resource[ATTRIBUTE]); + const checkedTelemetry = resources.filter(resource => resource[TELEMETRY]); + return checkedAttribute.length === 0 && checkedTelemetry.length === 0; + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts index 70334c9908..43c297df56 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts @@ -14,16 +14,19 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { Lwm2mDeviceProfileTransportConfigurationComponent } from './lwm2m-device-profile-transport-configuration.component'; -import { Lwm2mObjectListComponent } from './lwm2m-object-list.component'; -import { Lwm2mObserveAttrTelemetryComponent } from './lwm2m-observe-attr-telemetry.component'; -import { Lwm2mObserveAttrTelemetryResourceComponent } from './lwm2m-observe-attr-telemetry-resource.component'; -import { Lwm2mDeviceConfigServerComponent } from './lwm2m-device-config-server.component'; -import { Lwm2mObjectAddInstancesComponent } from './lwm2m-object-add-instances.component'; -import { Lwm2mObjectAddInstancesListComponent } from './lwm2m-object-add-instances-list.component'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '@app/shared/shared.module'; +import {NgModule} from '@angular/core'; +import {Lwm2mDeviceProfileTransportConfigurationComponent} from './lwm2m-device-profile-transport-configuration.component'; +import {Lwm2mObjectListComponent} from './lwm2m-object-list.component'; +import {Lwm2mObserveAttrTelemetryComponent} from './lwm2m-observe-attr-telemetry.component'; +import {Lwm2mObserveAttrTelemetryResourceComponent} from './lwm2m-observe-attr-telemetry-resource.component'; +import {Lwm2mAttributesDialogComponent} from './lwm2m-attributes-dialog.component'; +import {Lwm2mAttributesComponent} from './lwm2m-attributes.component'; +import {Lwm2mAttributesKeyListComponent} from './lwm2m-attributes-key-list.component'; +import {Lwm2mDeviceConfigServerComponent} from './lwm2m-device-config-server.component'; +import {Lwm2mObjectAddInstancesDialogComponent} from './lwm2m-object-add-instances-dialog.component'; +import {Lwm2mObjectAddInstancesListComponent} from './lwm2m-object-add-instances-list.component'; +import {CommonModule} from '@angular/common'; +import {SharedModule} from '@app/shared/shared.module'; @NgModule({ declarations: @@ -32,8 +35,11 @@ import { SharedModule } from '@app/shared/shared.module'; Lwm2mObjectListComponent, Lwm2mObserveAttrTelemetryComponent, Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mAttributesDialogComponent, + Lwm2mAttributesComponent, + Lwm2mAttributesKeyListComponent, Lwm2mDeviceConfigServerComponent, - Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesDialogComponent, Lwm2mObjectAddInstancesListComponent ], imports: [ @@ -45,8 +51,11 @@ import { SharedModule } from '@app/shared/shared.module'; Lwm2mObjectListComponent, Lwm2mObserveAttrTelemetryComponent, Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mAttributesDialogComponent, + Lwm2mAttributesComponent, + Lwm2mAttributesKeyListComponent, Lwm2mDeviceConfigServerComponent, - Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesDialogComponent, Lwm2mObjectAddInstancesListComponent ], providers: [ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts similarity index 78% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 24f2df4b39..678f7e260a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -16,7 +16,9 @@ export const PAGE_SIZE_LIMIT = 50; export const INSTANCES = 'instances'; +export const INSTANCE = 'instance'; export const RESOURCES = 'resources'; +export const ATTRIBUTE_LWM2M = 'attributeLwm2m'; export const CLIENT_LWM2M = 'clientLwM2M'; export const CLIENT_LWM2M_SETTINGS = 'clientLwM2mSettings'; export const OBSERVE_ATTR_TELEMETRY = 'observeAttrTelemetry'; @@ -33,7 +35,7 @@ export const DEFAULT_CLIENT_HOLD_OFF_TIME = 1; export const DEFAULT_LIFE_TIME = 300; export const DEFAULT_MIN_PERIOD = 1; export const DEFAULT_NOTIF_IF_DESIBLED = true; -export const DEFAULT_BINDING = 'U'; +export const DEFAULT_BINDING = 'UQ'; export const DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT = 0; export const LEN_MAX_PUBLIC_KEY_RPK = 182; export const LEN_MAX_PUBLIC_KEY_X509 = 3000; @@ -42,6 +44,44 @@ export const KEY_REGEXP_NUMBER = /^(\-?|\+?)\d*$/; export const INSTANCES_ID_VALUE_MIN = 0; export const INSTANCES_ID_VALUE_MAX = 65535; +export enum ATTRIBUTE_LWM2M_ENUM { + dim = 'dim', + ver = 'ver', + pmin = 'pmin', + pmax = 'pmax', + gt = 'gt', + lt = 'lt', + st = 'st' +} + +export const ATTRIBUTE_LWM2M_LABEL = new Map( + [ + [ATTRIBUTE_LWM2M_ENUM.dim, 'dim='], + [ATTRIBUTE_LWM2M_ENUM.ver, 'ver='], + [ATTRIBUTE_LWM2M_ENUM.pmin, 'pmin='], + [ATTRIBUTE_LWM2M_ENUM.pmax, 'pmax='], + [ATTRIBUTE_LWM2M_ENUM.gt, '>'], + [ATTRIBUTE_LWM2M_ENUM.lt, '<'], + [ATTRIBUTE_LWM2M_ENUM.st, 'st='], + + ] +); + +export const ATTRIBUTE_LWM2M_MAP = new Map( + [ + [ATTRIBUTE_LWM2M_ENUM.dim, 'Dimension'], + [ATTRIBUTE_LWM2M_ENUM.ver, 'Object version'], + [ATTRIBUTE_LWM2M_ENUM.pmin, 'Minimum period'], + [ATTRIBUTE_LWM2M_ENUM.pmax, 'Maximum period'], + [ATTRIBUTE_LWM2M_ENUM.gt, 'Greater than'], + [ATTRIBUTE_LWM2M_ENUM.lt, 'Lesser than'], + [ATTRIBUTE_LWM2M_ENUM.st, 'Step'], + + ] +); + +export const ATTRIBUTE_KEYS = Object.keys(ATTRIBUTE_LWM2M_ENUM) as string[]; + export enum SECURITY_CONFIG_MODE { PSK = 'PSK', RPK = 'RPK', @@ -54,12 +94,12 @@ export const SECURITY_CONFIG_MODE_NAMES = new Map( [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'], [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'], [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'], - [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'], + [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'] ] ); export interface ModelValue { - objectIds: string[] | null, + objectIds: string[], objectsList: ObjectLwM2M[] } @@ -90,7 +130,7 @@ interface BootstrapSecurityConfig { lwm2mServer: ServerSecurityConfig; } -export interface ProfileConfigModels { +export interface Lwm2mProfileConfigModels { clientLwM2mSettings: ClientLwM2mSettings; observeAttr: ObservableAttributes; bootstrap: BootstrapSecurityConfig; @@ -98,13 +138,15 @@ export interface ProfileConfigModels { } export interface ClientLwM2mSettings { - clientOnlyObserveAfterConnect: boolean; + clientOnlyObserveAfterConnect: number; } + export interface ObservableAttributes { observe: string[]; attribute: string[]; telemetry: string[]; keyName: {}; + attributeLwm2m: {}; } export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecurityConfig { @@ -151,21 +193,22 @@ function getDefaultProfileObserveAttrConfig(): ObservableAttributes { observe: [], attribute: [], telemetry: [], - keyName: {} + keyName: {}, + attributeLwm2m: {} }; } -function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { +export function getDefaultProfileConfig(hostname?: any): Lwm2mProfileConfigModels { return { - clientOnlyObserveAfterConnect: true + clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), + observeAttr: getDefaultProfileObserveAttrConfig(), + bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) }; } -export function getDefaultProfileConfig(hostname?: any): ProfileConfigModels { +function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { return { - clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), - observeAttr: getDefaultProfileObserveAttrConfig(), - bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) + clientOnlyObserveAfterConnect: 1 }; } @@ -175,11 +218,13 @@ export interface ResourceLwM2M { observe: boolean; attribute: boolean; telemetry: boolean; - keyName: string; + keyName: {}; + attributeLwm2m?: {}; } export interface Instance { id: number; + attributeLwm2m?: {}; resources: ResourceLwM2M[]; } @@ -190,11 +235,12 @@ export interface Instance { * mandatory == false => Optional */ export interface ObjectLwM2M { + id: number; keyId: string; name: string; multiple?: boolean; mandatory?: boolean; + attributeLwm2m?: {}; instances?: Instance []; } - diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts index 46e162f5fe..b190b0d293 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -29,6 +29,7 @@ import { EntityService } from '@core/http/entity.service'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { RuleChainService } from '@core/http/rule-chain.service'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { RuleChainType } from '@app/shared/models/rule-chain.models'; @Component({ selector: 'tb-rule-chain-autocomplete', @@ -187,8 +188,9 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI fetchRuleChain(searchText?: string): Observable>> { this.searchText = searchText; + // voba: at the moment device profiles are not supported by edge, so 'core' hardcoded return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, - 50, null, {ignoreLoading: true}); + 50, RuleChainType.CORE, {ignoreLoading: true}); } clear() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html new file mode 100644 index 0000000000..13deeacc17 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -0,0 +1,28 @@ + +
+
+ {{ customerTitle }} + edge.widget-datasource-error +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss new file mode 100644 index 0000000000..ec930cceb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2021 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-context(.tb-has-timewindow) { + .tb-edges-overview { + mat-toolbar { + height: 60px; + max-height: 60px; + .mat-toolbar-tools { + height: 60px; + max-height: 60px; + } + } + } +} + +:host { + .tb-edges-overview { + mat-toolbar.mat-table-toolbar:not([color="primary"]) { + background: transparent; + } + mat-toolbar { + min-height: 39px; + max-height: 39px; + .mat-toolbar-tools { + min-height: 39px; + max-height: 39px; + } + } + + .mat-subheader { + padding: 15px; + color: rgba(0,0,0,0.54); + font-size: 14px; + font-weight: 400; + } + + .tb-entities-nav-tree-panel { + overflow-x: auto; + overflow-y: auto; + } + } +} + +:host ::ng-deep { + .tb-nav-tree-container { + &.jstree-proton { + .jstree-anchor { + div.node-icon { + display: inline-block; + width: 22px; + height: 22px; + margin-right: 2px; + margin-bottom: 2px; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-position: center center; + background-size: 18px 18px; + } + + mat-icon.node-icon { + width: 22px; + min-width: 22px; + height: 22px; + min-height: 22px; + margin-right: 2px; + margin-bottom: 2px; + color: inherit; + vertical-align: middle; + + &.material-icons { + font-size: 18px; + line-height: 22px; + text-align: center; + } + } + + &.jstree-hovered:not(.jstree-clicked), + &.jstree-disabled { + div.node-icon { + opacity: .5; + } + } + } + } + } + + @media (max-width: 768px) { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-anchor { + div.node-icon { + width: 40px; + height: 40px; + margin: 0; + background-size: 24px 24px; + } + + mat-icon.node-icon { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + margin: 0; + + &.material-icons { + font-size: 24px; + line-height: 40px; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts new file mode 100644 index 0000000000..0b15def29f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -0,0 +1,197 @@ +/// +/// Copyright © 2016-2021 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 { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Datasource, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import { LoadNodesCallback } from '@shared/components/nav-tree.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { + EdgeGroupNodeData, + edgeGroupsNodeText, + edgeGroupsTypes, + entityNodeText, + EdgeOverviewNode, + EntityNodeData, + EntityNodeDatasource +} from '@home/components/widget/lib/edges-overview-widget.models'; +import { EdgeService } from '@core/http/edge.service'; +import { EntityService } from '@core/http/entity.service'; +import { TranslateService } from '@ngx-translate/core'; +import { PageLink } from '@shared/models/page/page-link'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { isDefined } from '@core/utils'; + +interface EdgesOverviewWidgetSettings { + enableDefaultTitle: boolean; +} + +@Component({ + selector: 'tb-edges-overview-widget', + templateUrl: './edges-overview-widget.component.html', + styleUrls: ['./edges-overview-widget.component.scss'] +}) +export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + public toastTargetId = 'edges-overview-' + this.utils.guid(); + public customerTitle: string = null; + public edgeIsDatasource: boolean = true; + + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private settings: EdgesOverviewWidgetSettings; + + private nodeIdCounter = 0; + + constructor(protected store: Store, + private edgeService: EdgeService, + private entityService: EntityService, + private translateService: TranslateService, + private utils: UtilsService, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources as Array; + this.settings = this.ctx.settings; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + public loadNodes: LoadNodesCallback = (node, cb) => { + const datasource: Datasource = this.datasources[0]; + if (node.id === '#' && datasource) { + if (datasource.type === DatasourceType.entity && datasource.entity.id.entityType === EntityType.EDGE) { + var selectedEdge: BaseData = datasource.entity; + this.updateTitle(selectedEdge); + this.getCustomerTitle(selectedEdge.id.id); + cb(this.loadNodesForEdge(selectedEdge)); + } else if (datasource.type === DatasourceType.function) { + cb(this.loadNodesForEdge(datasource.entity)); + } else { + this.edgeIsDatasource = false; + cb([]); + } + } + else if (node.data && node.data.entity.id.entityType === EntityType.EDGE) { + const edgeId = node.data.entity.id.id; + const entityType = node.data.entityType; + const pageLink = new PageLink(datasource.pageLink.pageSize); + this.entityService.getAssignedToEdgeEntitiesByType(edgeId, entityType, pageLink).subscribe( + (entities) => { + if (entities.data.length > 0) { + cb(this.entitiesToNodes(entities.data)); + } else { + cb([]); + } + } + ); + } else { + cb([]); + } + } + + private loadNodesForEdge(entity: BaseData): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + const authUser = getCurrentAuthUser(this.store); + var allowedGroupTypes: EntityType[] = edgeGroupsTypes; + if (authUser.authority === Authority.CUSTOMER_USER) { + allowedGroupTypes = edgeGroupsTypes.filter(type => type !== EntityType.RULE_CHAIN); + } + allowedGroupTypes.forEach((entityType) => { + const node: EdgeOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeGroupsNodeText(this.translateService, entityType), + children: true, + data: { + entityType, + entity, + internalId: entity.id.id + '_' + entityType + } as EdgeGroupNodeData + }; + nodes.push(node); + }); + return nodes; + } + + private createEntityNode(entity: BaseData): EdgeOverviewNode { + return { + id: (++this.nodeIdCounter)+'', + icon: false, + text: entityNodeText(entity), + children: false, + state: { + disabled: false + }, + data: { + entity: entity, + internalId: entity.id.id + } as EntityNodeData + } as EdgeOverviewNode; + } + + private entitiesToNodes(entities: BaseData[]): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + if (entities) { + entities.forEach((entity) => { + const node = this.createEntityNode(entity); + nodes.push(node); + }); + } + return nodes; + } + + private getCustomerTitle(edgeId: string) { + this.edgeService.getEdgeInfo(edgeId).subscribe( + (edge) => { + if (edge.customerTitle) { + this.customerTitle = this.translateService.instant('edge.assigned-to-customer', {customerTitle: edge.customerTitle}); + } else { + this.customerTitle = null; + } + this.cd.detectChanges(); + }); + } + + private initializeConfig(): void { + const edgeIsDatasource: boolean = this.datasources[0] && this.datasources[0].type === DatasourceType.entity && this.datasources[0].entity.id.entityType === EntityType.EDGE; + if (edgeIsDatasource) { + const edge = this.datasources[0].entity; + this.updateTitle(edge); + } + } + + private updateTitle(edge: BaseData): void { + const displayDefaultTitle: boolean = isDefined(this.settings.enableDefaultTitle) ? this.settings.enableDefaultTitle : false; + this.ctx.widgetTitle = displayDefaultTitle ? `${edge.name} Quick Overview` : this.widgetConfig.title; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts new file mode 100644 index 0000000000..e13ef2797f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2021 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 { NavTreeNode } from '@shared/components/nav-tree.component'; +import { Datasource } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { BaseData, HasId } from '@shared/models/base-data'; + +export interface EntityNodeDatasource extends Datasource { + nodeId: string; +} + +export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { + const nodeIcon = materialIconByEntityType(entityType); + const nodeText = textForEdgeGroupsType(translate, entityType); + return nodeIcon + nodeText; +} + +export function entityNodeText(entity: any): string { + const nodeIcon = materialIconByEntityType(entity.id.entityType); + const nodeText = entity.name; + return nodeIcon + nodeText; +} + +export function materialIconByEntityType(entityType: EntityType): string { + let materialIcon = 'insert_drive_file'; + switch (entityType) { + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + case EntityType.RULE_CHAIN: + materialIcon = 'settings_ethernet'; + break; + } + return '' + materialIcon + ''; +} + +export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { + let textForEdgeGroupsType: string = ''; + switch (entityType) { + case EntityType.DEVICE: + textForEdgeGroupsType = 'device.devices'; + break; + case EntityType.ASSET: + textForEdgeGroupsType = 'asset.assets'; + break; + case EntityType.DASHBOARD: + textForEdgeGroupsType = 'dashboard.dashboards'; + break; + case EntityType.ENTITY_VIEW: + textForEdgeGroupsType = 'entity-view.entity-views'; + break; + case EntityType.RULE_CHAIN: + textForEdgeGroupsType = 'rulechain.rulechains'; + break; + } + return translate.instant(textForEdgeGroupsType); +} + +export const edgeGroupsTypes: EntityType[] = [ + EntityType.ASSET, + EntityType.DEVICE, + EntityType.ENTITY_VIEW, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN +] + +export interface EdgeOverviewNode extends NavTreeNode { + data?: EdgeOverviewNodeData; +} + +export type EdgeOverviewNodeData = EdgeGroupNodeData | EntityNodeData; + +export interface EdgeGroupNodeData extends BaseEdgeOverviewNodeData { + entityType: EntityType; + entity: BaseData; +} + +export interface EntityNodeData extends BaseEdgeOverviewNodeData { + entity: BaseData; +} + +export interface BaseEdgeOverviewNodeData { + internalId: string; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 2d080bc8e9..2ad0fb1612 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -37,6 +37,7 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; +import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; @NgModule({ declarations: @@ -47,6 +48,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, DateRangeNavigatorWidgetComponent, DateRangeNavigatorPanelComponent, MultipleInputWidgetComponent, @@ -67,6 +69,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, RpcWidgetsModule, DateRangeNavigatorWidgetComponent, MultipleInputWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts index f5b26df29c..bcaaa71bf9 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts @@ -28,6 +28,7 @@ import { EntityViewService } from '@core/http/entity-view.service'; import { DashboardService } from '@core/http/dashboard.service'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; +import { EdgeService } from '@core/http/edge.service'; export interface AddEntitiesToCustomerDialogData { customerId: string; @@ -57,6 +58,7 @@ export class AddEntitiesToCustomerDialogComponent extends @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private dashboardService: DashboardService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, @@ -79,6 +81,10 @@ export class AddEntitiesToCustomerDialogComponent extends this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; this.assignToCustomerText = 'asset.assign-asset-to-customer-text'; break; + case EntityType.EDGE: + this.assignToCustomerTitle = 'edge.assign-edge-to-customer'; + this.assignToCustomerText = 'edge.assign-edge-to-customer-text'; + break; case EntityType.ENTITY_VIEW: this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; this.assignToCustomerText = 'entity-view.assign-entity-view-to-customer-text'; @@ -122,6 +128,8 @@ export class AddEntitiesToCustomerDialogComponent extends return this.deviceService.assignDeviceToCustomer(customerId, entityId); case EntityType.ASSET: return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.EDGE: + return this.edgeService.assignEdgeToCustomer(customerId, entityId); case EntityType.ENTITY_VIEW: return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); case EntityType.DASHBOARD: diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html new file mode 100644 index 0000000000..82f166d71a --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html @@ -0,0 +1,57 @@ + + + +

{{ assignToEdgeTitle | translate }}

+ + +
+ + +
+
+
+ {{ assignToEdgeText | translate }} + + +
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts new file mode 100644 index 0000000000..1cd9ba87ba --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts @@ -0,0 +1,146 @@ +/// +/// Copyright © 2016-2021 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, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { EdgeService } from '@core/http/edge.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { forkJoin, Observable } from 'rxjs'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleChainType } from '@shared/models/rule-chain.models'; + +export interface AddEntitiesToEdgeDialogData { + edgeId: string; + entityType: EntityType; +} + +@Component({ + selector: 'tb-add-entities-to-edge-dialog', + templateUrl: './add-entities-to-edge-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToEdgeDialogComponent}], + styleUrls: [] +}) +export class AddEntitiesToEdgeDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + addEntitiesToEdgeFormGroup: FormGroup; + + submitted = false; + + entityType: EntityType; + subType: string; + + assignToEdgeTitle: string; + assignToEdgeText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToEdgeDialogData, + private deviceService: DeviceService, + private edgeService: EdgeService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private dashboardService: DashboardService, + private ruleChainService: RuleChainService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.entityType = data.entityType; + } + + ngOnInit(): void { + this.addEntitiesToEdgeFormGroup = this.fb.group({ + entityIds: [null, [Validators.required]] + }); + this.subType = ''; + switch (this.data.entityType) { + case EntityType.DEVICE: + this.assignToEdgeTitle = 'device.assign-device-to-edge-title'; + this.assignToEdgeText = 'device.assign-device-to-edge-text'; + break; + case EntityType.RULE_CHAIN: + this.assignToEdgeTitle = 'rulechain.assign-rulechain-to-edge-title'; + this.assignToEdgeText = 'rulechain.assign-rulechain-to-edge-text'; + this.subType = RuleChainType.EDGE; + break; + case EntityType.ASSET: + this.assignToEdgeTitle = 'asset.assign-asset-to-edge-title'; + this.assignToEdgeText = 'asset.assign-asset-to-edge-text'; + break; + case EntityType.ENTITY_VIEW: + this.assignToEdgeTitle = 'entity-view.assign-entity-view-to-edge-title'; + this.assignToEdgeText = 'entity-view.assign-entity-view-to-edge-text'; + break; + case EntityType.DASHBOARD: + this.assignToEdgeTitle = 'dashboard.assign-dashboard-to-edge-title'; + this.assignToEdgeText = 'dashboard.assign-dashboard-to-edge-text'; + break; + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + assign(): void { + this.submitted = true; + const entityIds: Array = this.addEntitiesToEdgeFormGroup.get('entityIds').value; + const tasks: Observable[] = []; + entityIds.forEach( + (entityId) => { + tasks.push(this.getAssignToEdgeTask(this.data.edgeId, entityId)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getAssignToEdgeTask(edgeId: string, entityId: string): Observable { + switch (this.data.entityType) { + case EntityType.DEVICE: + return this.deviceService.assignDeviceToEdge(edgeId, entityId); + case EntityType.ASSET: + return this.assetService.assignAssetToEdge(edgeId, entityId); + case EntityType.ENTITY_VIEW: + return this.entityViewService.assignEntityViewToEdge(edgeId, entityId); + case EntityType.DASHBOARD: + return this.dashboardService.assignDashboardToEdge(edgeId, entityId); + case EntityType.RULE_CHAIN: + return this.ruleChainService.assignRuleChainToEdge(edgeId, entityId); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts index 8d8230c5ee..2574bf8ae1 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts @@ -28,6 +28,7 @@ import { AssetService } from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; +import { EdgeService } from '@core/http/edge.service'; export interface AssignToCustomerDialogData { entityIds: Array; @@ -57,6 +58,7 @@ export class AssignToCustomerDialogComponent extends @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, @@ -77,6 +79,10 @@ export class AssignToCustomerDialogComponent extends this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; this.assignToCustomerText = 'asset.assign-to-customer-text'; break; + case EntityType.EDGE: + this.assignToCustomerTitle = 'edge.assign-edge-to-customer'; + this.assignToCustomerText = 'edge.assign-to-customer-text'; + break; case EntityType.ENTITY_VIEW: this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; this.assignToCustomerText = 'entity-view.assign-to-customer-text'; @@ -116,6 +122,8 @@ export class AssignToCustomerDialogComponent extends return this.deviceService.assignDeviceToCustomer(customerId, entityId); case EntityType.ASSET: return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.EDGE: + return this.edgeService.assignEdgeToCustomer(customerId, entityId); case EntityType.ENTITY_VIEW: return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); } diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts index 1a5cc337df..91079ad040 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts @@ -20,12 +20,14 @@ import { SharedModule } from '@app/shared/shared.module'; import { AssignToCustomerDialogComponent } from '@modules/home/dialogs/assign-to-customer-dialog.component'; import { AddEntitiesToCustomerDialogComponent } from '@modules/home/dialogs/add-entities-to-customer-dialog.component'; import { HomeDialogsService } from './home-dialogs.service'; +import { AddEntitiesToEdgeDialogComponent } from '@home/dialogs/add-entities-to-edge-dialog.component'; @NgModule({ declarations: [ AssignToCustomerDialogComponent, - AddEntitiesToCustomerDialogComponent + AddEntitiesToCustomerDialogComponent, + AddEntitiesToEdgeDialogComponent ], imports: [ CommonModule, @@ -33,7 +35,8 @@ import { HomeDialogsService } from './home-dialogs.service'; ], exports: [ AssignToCustomerDialogComponent, - AddEntitiesToCustomerDialogComponent + AddEntitiesToCustomerDialogComponent, + AddEntitiesToEdgeDialogComponent ], providers: [ HomeDialogsService diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts index 69b183bb3f..fc7fd73384 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -36,6 +36,8 @@ export class HomeDialogsService { return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file'); case EntityType.ASSET: return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file'); + case EntityType.EDGE: + return this.openImportDialogCSV(entityType, 'edge.import', 'edge.edge-file'); } } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 3bcc4594c9..66b9a152c7 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && (assetScope === 'customer' || assetScope === 'tenant') && isAssignedToCustomer(entity)"> {{ (entity?.customerIsPublic ? 'asset.make-private' : 'asset.unassign-from-customer') | translate }} + + + + + + + +
+ + + + + +
+
+ + + + +
+
+
+ + edge.assignedToCustomer + + +
+ {{ 'edge.edge-public' | translate }} +
+
+
+ + edge.name + + + {{ 'edge.name-required' | translate }} + + + + +
+
+ + edge.edge-license-key + + + {{ 'edge.edge-license-key-required' | translate }} + + +
+
+
edge.cloud-endpoint-hint
+ + edge.cloud-endpoint + + + {{ 'edge.cloud-endpoint-required' | translate }} + + +
+
+
+ + edge.edge-key + + + +
+
+ + edge.edge-secret + + + +
+
+ + edge.label + + +
+ + edge.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.scss b/ui-ngx/src/app/modules/home/pages/edge/edge.component.scss new file mode 100644 index 0000000000..3abcc4c738 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2021 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 { + +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts new file mode 100644 index 0000000000..007176ce24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2021 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 { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { EdgeInfo } from '@shared/models/edge.models'; +import { TranslateService } from '@ngx-translate/core'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { generateSecret, guid } from '@core/utils'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-edge', + templateUrl: './edge.component.html', + styleUrls: ['./edge.component.scss'] +}) +export class EdgeComponent extends EntityComponent { + + entityType = EntityType; + + edgeScope: 'tenant' | 'customer' | 'customer_user'; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: EdgeInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.edgeScope = this.entitiesTableConfig.componentsData.edgeScope; + this.entityForm.patchValue({ + cloudEndpoint: window.location.origin + }); + super.ngOnInit(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isAssignedToCustomer(entity: EdgeInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + + buildForm(entity: EdgeInfo): FormGroup { + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity?.type ? entity.type : 'default', [Validators.required]], + label: [entity ? entity.label : ''], + cloudEndpoint: [null, [Validators.required]], + edgeLicenseKey: ['', [Validators.required]], + routingKey: this.fb.control({value: entity ? entity.routingKey : null, disabled: true}), + secret: this.fb.control({value: entity ? entity.secret : null, disabled: true}), + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + } + ) + } + ); + this.generateRoutingKeyAndSecret(entity, form); + return form; + } + + updateForm(entity: EdgeInfo) { + this.entityForm.patchValue({ + name: entity.name, + type: entity.type, + label: entity.label, + cloudEndpoint: entity.cloudEndpoint ? entity.cloudEndpoint : window.location.origin, + edgeLicenseKey: entity.edgeLicenseKey, + routingKey: entity.routingKey, + secret: entity.secret, + additionalInfo: { + description: entity.additionalInfo ? entity.additionalInfo.description : '' + } + }); + this.generateRoutingKeyAndSecret(entity, this.entityForm); + } + + updateFormState() { + super.updateFormState(); + this.entityForm.get('routingKey').disable({emitEvent: false}); + this.entityForm.get('secret').disable({emitEvent: false}); + } + + onEdgeIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('edge.id-copied-message'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + onEdgeInfoCopied(type: string) { + const message = type === 'key' ? 'edge.edge-key-copied-message' + : 'edge.edge-secret-copied-message'; + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant(message), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + private generateRoutingKeyAndSecret(entity: EdgeInfo, form: FormGroup) { + if (entity && !entity.id) { + form.get('routingKey').patchValue(guid(), {emitEvent: false}); + form.get('secret').patchValue(generateSecret(20), {emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts new file mode 100644 index 0000000000..1fc00a023b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2021 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '@home/dialogs/home-dialogs.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { EdgeRoutingModule } from '@home/pages/edge/edge-routing.module'; +import { EdgeComponent } from '@modules/home/pages/edge/edge.component'; +import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component'; +import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component'; + +@NgModule({ + declarations: [ + EdgeComponent, + EdgeTableHeaderComponent, + EdgeTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeDialogsModule, + HomeComponentsModule, + EdgeRoutingModule + ] +}) + +export class EdgeModule { } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts new file mode 100644 index 0000000000..96251948d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts @@ -0,0 +1,553 @@ +/// +/// Copyright © 2016-2021 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 { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { Edge, EdgeInfo } from '@shared/models/edge.models'; +import { EdgeService } from '@core/http/edge.service'; +import { EdgeComponent } from '@home/pages/edge/edge.component'; +import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component'; +import { EdgeId } from '@shared/models/id/edge-id'; +import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; + +@Injectable() +export class EdgesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + private customerId: string; + + constructor(private store: Store, + private broadcast: BroadcastService, + private edgeService: EdgeService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.EDGE; + this.config.entityComponent = EdgeComponent; + this.config.entityTabsComponent = EdgeTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.EDGE); + this.config.entityResources = entityTypeResources.get(EntityType.EDGE); + + this.config.deleteEntityTitle = edge => this.translate.instant('edge.delete-edge-title', {edgeName: edge.name}); + this.config.deleteEntityContent = () => this.translate.instant('edge.delete-edge-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('edge.delete-edges-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('edge.delete-edges-text'); + + this.config.loadEntity = id => this.edgeService.getEdgeInfo(id.id); + this.config.saveEntity = edge => { + return this.edgeService.saveEdge(edge).pipe( + tap(() => { + this.broadcast.broadcast('edgeSaved'); + }), + mergeMap((savedEdge) => this.edgeService.getEdgeInfo(savedEdge.id.id) + )); + }; + this.config.onEntityAction = action => this.onEdgeAction(action); + this.config.detailsReadonly = () => this.config.componentsData.edgeScope === 'customer_user'; + this.config.headerComponent = EdgeTableHeaderComponent; + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + edgeScope: route.data.edgesType, + edgeType: '' + }; + this.customerId = routeParams.customerId; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.edgeScope = 'customer_user'; + this.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-edges'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('edge.edge-instances'); + } + } else { + this.config.tableTitle = this.translate.instant('edge.edge-instances'); + } + this.config.columns = this.configureColumns(this.config.componentsData.edgeScope); + this.configureEntityFunctions(this.config.componentsData.edgeScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.edgeScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.edgeScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.edgeScope); + this.config.addEnabled = this.config.componentsData.edgeScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.edgeScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.edgeScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(edgeScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'edge.name', '25%'), + new EntityTableColumn('type', 'edge.edge-type', '25%'), + new EntityTableColumn('label', 'edge.label', '25%') + ]; + if (edgeScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer', '25%'), + new EntityTableColumn('customerIsPublic', 'edge.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false) + ); + } + return columns; + } + + configureEntityFunctions(edgeScope: string): void { + if (edgeScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getTenantEdgeInfos(pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.deleteEdge(id.id); + } + if (edgeScope === 'customer') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getCustomerEdgeInfos(this.customerId, pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.unassignEdgeFromCustomer(id.id); + } + if (edgeScope === 'customer_user') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getCustomerEdgeInfos(this.customerId, pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.unassignEdgeFromCustomer(id.id); + } + } + + configureCellActions(edgeScope: string): Array> { + const actions: Array> = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.make-public'), + icon: 'share', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('edge.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.manage-edge-assets'), + icon: 'domain', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) + }, + { + name: this.translate.instant('edge.manage-edge-devices'), + icon: 'devices_other', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) + }, + { + name: this.translate.instant('edge.manage-edge-entity-views'), + icon: 'view_quilt', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) + }, + { + name: this.translate.instant('edge.manage-edge-dashboards'), + icon: 'dashboard', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) + }, + { + name: this.translate.instant('edge.manage-edge-rulechains'), + icon: 'settings_ethernet', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.RULE_CHAIN) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + ); + } + if (edgeScope === 'customer_user') { + actions.push( + { + name: this.translate.instant('edge.manage-edge-assets'), + icon: 'domain', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) + }, + { + name: this.translate.instant('edge.manage-edge-devices'), + icon: 'devices_other', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) + }, + { + name: this.translate.instant('edge.manage-edge-entity-views'), + icon: 'view_quilt', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) + }, + { + name: this.translate.instant('edge.manage-edge-dashboards'), + icon: 'dashboard', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) + } + ); + } + return actions; + } + + configureGroupActions(edgeScope: string): Array> { + const actions: Array> = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.assign-edge-to-customer-text'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignEdgesFromCustomer($event, entities) + } + ); + } + return actions; + } + + configureAddActions(edgeScope: string): Array { + const actions: Array = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.add-edge-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('edge.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importEdges($event) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.assign-new-edge'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addEdgesToCustomer($event) + } + ); + } + return actions; + } + + importEdges($event: Event) { + this.homeDialogs.importEntities(EntityType.EDGE).subscribe((res) => { + if (res) { + this.broadcast.broadcast('edgeSaved'); + this.config.table.updateData(); + } + }); + } + + addEdgesToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.customerId, + entityType: EntityType.EDGE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, edge: Edge) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('edge.make-public-edge-title', {edgeName: edge.name}), + this.translate.instant('edge.make-public-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.edgeService.makeEdgePublic(edge.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + openEdgeEntitiesByType($event: Event, edge: Edge, entityType: EntityType) { + if ($event) { + $event.stopPropagation(); + } + let suffix: string; + switch (entityType) { + case EntityType.DEVICE: + suffix = 'devices'; + break; + case EntityType.ASSET: + suffix = 'assets'; + break; + case EntityType.EDGE: + suffix = 'assets'; + break; + case EntityType.ENTITY_VIEW: + suffix = 'entityViews'; + break; + case EntityType.DASHBOARD: + suffix = 'dashboards'; + break; + case EntityType.RULE_CHAIN: + suffix = 'ruleChains'; + break; + } + this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`); + } + + assignToCustomer($event: Event, edgesIds: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: edgesIds, + entityType: EntityType.EDGE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, edge: EdgeInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = edge.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('edge.make-private-edge-title', {edgeName: edge.name}); + content = this.translate.instant('edge.make-private-edge-text'); + } else { + title = this.translate.instant('edge.unassign-edge-title', {edgeName: edge.name}); + content = this.translate.instant('edge.unassign-edge-text'); + } + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.edgeService.unassignEdgeFromCustomer(edge.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignEdgesFromCustomer($event: Event, edges: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('edge.unassign-edge-title', {count: edges.length}), + this.translate.instant('edge.unassign-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + edges.forEach( + (edge) => { + tasks.push(this.edgeService.unassignEdgeFromCustomer(edge.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + syncEdge($event, edge) { + if ($event) { + $event.stopPropagation(); + } + this.edgeService.syncEdge(edge.id.id).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('edge.sync-process-started-successfully'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + ); + } + + onEdgeAction(action: EntityAction): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, [action.entity.id]); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + case 'openEdgeAssets': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.ASSET); + return true; + case 'openEdgeDevices': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.DEVICE); + return true; + case 'openEdgeEntityViews': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.ENTITY_VIEW); + return true; + case 'openEdgeDashboards': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.DASHBOARD); + return true; + case 'openEdgeRuleChains': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.RULE_CHAIN); + return true; + case 'syncEdge': + this.syncEdge(action.event, action.entity); + return true; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index cf1a9bc936..5ce3b05569 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && (entityViewScope === 'customer' || entityViewScope === 'tenant') && isAssignedToCustomer(entity)"> {{ (entity?.customerIsPublic ? 'entity-view.make-private' : 'entity-view.unassign-from-customer') | translate }} + + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts index dd7f62e493..3d8eae1e7a 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts @@ -31,6 +31,8 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod }) export class RuleChainComponent extends EntityComponent { + ruleChainScope: 'tenant' | 'edges' | 'edge'; + constructor(protected store: Store, protected translate: TranslateService, @Inject('entity') protected entityValue: RuleChain, @@ -39,6 +41,11 @@ export class RuleChainComponent extends EntityComponent { super(store, fb, entityValue, entitiesTableConfigValue); } + ngOnInit() { + this.ruleChainScope = this.entitiesTableConfig.componentsData.ruleChainScope; + super.ngOnInit(); + } + hideDelete() { if (this.entitiesTableConfig) { return !this.entitiesTableConfig.deleteEnabled(this.entity); @@ -78,4 +85,30 @@ export class RuleChainComponent extends EntityComponent { horizontalPosition: 'right' })); } + + isEdgeRootRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return this.entitiesTableConfig.componentsData.edge?.rootRuleChainId?.id == this.entityValue.id.id; + } else { + return false; + } + } + + isAutoAssignToEdgeRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return !this.entityValue.root && + this.entitiesTableConfig.componentsData?.autoAssignToEdgeRuleChainIds?.includes(this.entityValue.id.id); + } else { + return false; + } + } + + isNotAutoAssignToEdgeRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return !this.entityValue.root && + !this.entitiesTableConfig.componentsData?.autoAssignToEdgeRuleChainIds?.includes(this.entityValue.id.id); + } else { + return false; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts index f2cf4b9da8..2b9572b7fa 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts @@ -16,118 +16,281 @@ import { Injectable } from '@angular/core'; -import { Resolve, Router } from '@angular/router'; +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; import { + CellActionDescriptor, checkBoxCell, DateEntityTableColumn, + EntityColumn, EntityTableColumn, - EntityTableConfig + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor } from '@home/models/entity/entities-table-config.models'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { RuleChain } from '@shared/models/rule-chain.models'; +import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainComponent } from '@modules/home/pages/rulechain/rulechain.component'; import { DialogService } from '@core/services/dialog.service'; import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { ItemBufferService } from '@core/services/item-buffer.service'; +import { EdgeService } from '@core/http/edge.service'; +import { forkJoin, Observable } from 'rxjs'; +import { + AddEntitiesToEdgeDialogComponent, + AddEntitiesToEdgeDialogData +} from '@home/dialogs/add-entities-to-edge-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { isUndefined } from '@core/utils'; +import { PageLink } from '@shared/models/page/page-link'; +import { Edge } from '@shared/models/edge.models'; +import { mergeMap } from 'rxjs/operators'; +import { PageData } from '@shared/models/page/page-data'; @Injectable() export class RuleChainsTableConfigResolver implements Resolve> { private readonly config: EntityTableConfig = new EntityTableConfig(); + private edge: Edge; constructor(private ruleChainService: RuleChainService, private dialogService: DialogService, + private dialog: MatDialog, private importExport: ImportExportService, private itembuffer: ItemBufferService, + private edgeService: EdgeService, private translate: TranslateService, private datePipe: DatePipe, private router: Router) { - this.config.entityType = EntityType.RULE_CHAIN; this.config.entityComponent = RuleChainComponent; this.config.entityTabsComponent = RuleChainTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.RULE_CHAIN); this.config.entityResources = entityTypeResources.get(EntityType.RULE_CHAIN); - this.config.columns.push( - new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('name', 'rulechain.name', '100%'), - new EntityTableColumn('root', 'rulechain.root', '60px', - entity => { - return checkBoxCell(entity.root); - }), - ); - - this.config.addActionDescriptors.push( - { - name: this.translate.instant('rulechain.create-new-rulechain'), - icon: 'insert_drive_file', - isEnabled: () => true, - onAction: ($event) => this.config.table.addEntity($event) - }, - { - name: this.translate.instant('rulechain.import'), - icon: 'file_upload', - isEnabled: () => true, - onAction: ($event) => this.importRuleChain($event) - } - ); - - this.config.cellActionDescriptors.push( - { - name: this.translate.instant('rulechain.open-rulechain'), - icon: 'settings_ethernet', - isEnabled: () => true, - onAction: ($event, entity) => this.openRuleChain($event, entity) - }, - { - name: this.translate.instant('rulechain.export'), - icon: 'file_download', - isEnabled: () => true, - onAction: ($event, entity) => this.exportRuleChain($event, entity) - }, - { - name: this.translate.instant('rulechain.set-root'), - icon: 'flag', - isEnabled: (ruleChain) => !ruleChain.root, - onAction: ($event, entity) => this.setRootRuleChain($event, entity) - } - ); - this.config.deleteEntityTitle = ruleChain => this.translate.instant('rulechain.delete-rulechain-title', { ruleChainName: ruleChain.name }); this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('rulechain.delete-rulechains-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('rulechain.delete-rulechains-text'); - - this.config.entitiesFetchFunction = pageLink => this.ruleChainService.getRuleChains(pageLink); this.config.loadEntity = id => this.ruleChainService.getRuleChain(id.id); - this.config.saveEntity = ruleChain => this.ruleChainService.saveRuleChain(ruleChain); + this.config.saveEntity = ruleChain => this.saveRuleChain(ruleChain); this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id); this.config.onEntityAction = action => this.onRuleChainAction(action); - this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root; - this.config.entitySelectionEnabled = (ruleChain) => ruleChain && !ruleChain.root; } - resolve(): EntityTableConfig { - this.config.tableTitle = this.translate.instant('rulechain.rulechains'); - + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + const edgeId = route.params?.edgeId; + const ruleChainScope = route.data?.ruleChainsType ? route.data?.ruleChainsType : 'tenant'; + this.config.componentsData = { + ruleChainScope, + edgeId + }; + this.config.columns = this.configureEntityTableColumns(ruleChainScope); + this.config.entitiesFetchFunction = this.configureEntityFunctions(ruleChainScope, edgeId); + this.config.groupActionDescriptors = this.configureGroupActions(ruleChainScope); + this.config.addActionDescriptors = this.configureAddActions(ruleChainScope); + this.config.cellActionDescriptors = this.configureCellActions(ruleChainScope); + if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { + this.config.entitySelectionEnabled = ruleChain => ruleChain && !ruleChain.root; + this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root; + this.config.entitiesDeleteEnabled = true; + this.config.tableTitle = this.configureTableTitle(ruleChainScope, null); + } else if (ruleChainScope === 'edge') { + this.config.entitySelectionEnabled = ruleChain => this.config.componentsData.edge.rootRuleChainId.id !== ruleChain.id.id; + this.edgeService.getEdge(edgeId).subscribe(edge => { + this.config.componentsData.edge = edge; + this.config.tableTitle = this.configureTableTitle(ruleChainScope, edge); + }); + this.config.entitiesDeleteEnabled = false; + } return this.config; } + configureEntityTableColumns(ruleChainScope: string): Array> { + const columns: Array> = []; + columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'rulechain.name', '100%') + ); + if (ruleChainScope === 'tenant' || ruleChainScope === 'edge') { + columns.push( + new EntityTableColumn('root', 'rulechain.root', '60px', + entity => { + if (ruleChainScope === 'edge') { + return checkBoxCell((this.config.componentsData.edge.rootRuleChainId.id === entity.id.id)); + } else { + return checkBoxCell(entity.root); + } + }) + ); + } else if (ruleChainScope === 'edges') { + columns.push( + new EntityTableColumn('root', 'rulechain.edge-template-root', '60px', + entity => { + return checkBoxCell(entity.root); + }), + new EntityTableColumn('assignToEdge', 'rulechain.assign-to-edge', '60px', + entity => { + return checkBoxCell(this.isAutoAssignToEdgeRuleChain(entity)); + }) + ); + } + return columns; + } + + configureAddActions(ruleChainScope: string): Array { + const actions: Array = []; + if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { + actions.push( + { + name: this.translate.instant('rulechain.create-new-rulechain'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('rulechain.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importRuleChain($event) + } + ); + } + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.assign-new-rulechain'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addRuleChainsToEdge($event) + } + ); + } + return actions; + } + + configureEntityFunctions(ruleChainScope: string, edgeId: string): (pageLink) => Observable> { + if (ruleChainScope === 'tenant') { + return pageLink => this.fetchRuleChains(pageLink); + } else if (ruleChainScope === 'edges') { + return pageLink => this.fetchEdgeRuleChains(pageLink); + } else if (ruleChainScope === 'edge') { + return pageLink => this.ruleChainService.getEdgeRuleChains(edgeId, pageLink); + } + } + + configureTableTitle(ruleChainScope: string, edge: Edge): string { + if (ruleChainScope === 'tenant') { + return this.translate.instant('rulechain.rulechains'); + } else if (ruleChainScope === 'edges') { + return this.translate.instant('edge.rulechain-templates'); + } else if (ruleChainScope === 'edge') { + return this.config.tableTitle = edge.name + ': ' + this.translate.instant('edge.rulechains'); + } + } + + configureGroupActions(ruleChainScope: string): Array> { + const actions: Array> = []; + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.unassign-rulechains'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignRuleChainsFromEdge($event, entities) + } + ); + } + return actions; + } + + configureCellActions(ruleChainScope: string): Array> { + const actions: Array> = []; + if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { + actions.push( + { + name: this.translate.instant('rulechain.open-rulechain'), + icon: 'settings_ethernet', + isEnabled: () => true, + onAction: ($event, entity) => this.openRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.exportRuleChain($event, entity) + } + ); + if (ruleChainScope === 'tenant') { + actions.push( + { + name: this.translate.instant('rulechain.set-root'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setRootRuleChain($event, entity) + } + ); + } + if (ruleChainScope === 'edges') { + actions.push( + { + name: this.translate.instant('rulechain.set-edge-template-root-rulechain'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.set-auto-assign-to-edge'), + icon: 'bookmark_outline', + isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity), + onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.unset-auto-assign-to-edge'), + icon: 'bookmark', + isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity), + onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity) + } + ); + } + } + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.set-root'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setRootRuleChain($event, entity) + }, + { + name: this.translate.instant('edge.unassign-from-edge'), + icon: 'assignment_return', + isEnabled: (entity) => entity.id.id !== this.config.componentsData.edge.rootRuleChainId.id, + onAction: ($event, entity) => this.unassignFromEdge($event, entity) + } + ); + } + return actions; + } + importRuleChain($event: Event) { if ($event) { $event.stopPropagation(); } - this.importExport.importRuleChain().subscribe((ruleChainImport) => { + const expectedRuleChainType = this.config.componentsData.ruleChainScope === 'tenant' ? RuleChainType.CORE : RuleChainType.EDGE; + this.importExport.importRuleChain(expectedRuleChainType).subscribe((ruleChainImport) => { if (ruleChainImport) { this.itembuffer.storeRuleChainImport(ruleChainImport); - this.router.navigateByUrl(`ruleChains/ruleChain/import`); + if (this.config.componentsData.ruleChainScope === 'edges') { + this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`); + } else { + this.router.navigateByUrl(`ruleChains/ruleChain/import`); + } } }); } @@ -136,7 +299,25 @@ export class RuleChainsTableConfigResolver implements Resolve { if (res) { - this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe( - () => { - this.config.table.updateData(); - } - ); + if (this.config.componentsData.ruleChainScope === 'edge') { + this.ruleChainService.setEdgeRootRuleChain(this.config.componentsData.edgeId, ruleChain.id.id).subscribe( + (edge) => { + this.config.componentsData.edge = edge; + this.config.table.updateData(); + } + ); + } else { + this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } } } ); @@ -179,8 +369,207 @@ export class RuleChainsTableConfigResolver implements Resolve { + if (res) { + this.ruleChainService.setEdgeTemplateRootRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + addRuleChainsToEdge($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToEdgeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + edgeId: this.config.componentsData.edgeId, + entityType: EntityType.RULE_CHAIN + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.edgeService.findMissingToRelatedRuleChains(this.config.componentsData.edgeId).subscribe( + (missingRuleChains) => { + if (missingRuleChains && Object.keys(missingRuleChains).length > 0) { + const formattedMissingRuleChains: Array = new Array(); + for (const missingRuleChain of Object.keys(missingRuleChains)) { + const arrayOfMissingRuleChains = missingRuleChains[missingRuleChain]; + const tmp = '- \'' + missingRuleChain + '\': \'' + arrayOfMissingRuleChains.join('\', ') + '\''; + formattedMissingRuleChains.push(tmp); + } + const message = this.translate.instant('edge.missing-related-rule-chains-text', + {missingRuleChains: formattedMissingRuleChains.join('
')}); + this.dialogService.alert(this.translate.instant('edge.missing-related-rule-chains-title'), + message, this.translate.instant('action.close'), true).subscribe( + () => { + this.config.table.updateData(); + } + ); + } else { + this.config.table.updateData(); + } + } + ); + } + } + ); + } + + unassignFromEdge($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unassign-rulechain-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.unassign-rulechain-from-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.unassignRuleChainFromEdge(this.config.componentsData.edgeId, ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignRuleChainsFromEdge($event: Event, ruleChains: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unassign-rulechains-from-edge-title', {count: ruleChains.length}), + this.translate.instant('rulechain.unassign-rulechains-from-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + ruleChains.forEach( + (ruleChain) => { + tasks.push(this.ruleChainService.unassignRuleChainFromEdge(this.config.componentsData.edgeId, ruleChain.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + setAutoAssignToEdgeRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.set-auto-assign-to-edge-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.set-auto-assign-to-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.setAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unsetAutoAssignToEdgeRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unset-auto-assign-to-edge-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.unset-auto-assign-to-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.unsetAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + isNonRootRuleChain(ruleChain: RuleChain) { + if (this.config.componentsData.ruleChainScope === 'edge') { + return this.config.componentsData.edge.rootRuleChainId && + this.config.componentsData.edge.rootRuleChainId.id !== ruleChain.id.id; + } + return !ruleChain.root; + } + + isAutoAssignToEdgeRuleChain(ruleChain) { + return !ruleChain.root && this.config.componentsData.autoAssignToEdgeRuleChainIds.includes(ruleChain.id.id); + } + + isNotAutoAssignToEdgeRuleChain(ruleChain) { + return !ruleChain.root && !this.config.componentsData.autoAssignToEdgeRuleChainIds.includes(ruleChain.id.id); + } + + fetchRuleChains(pageLink: PageLink) { + return this.ruleChainService.getRuleChains(pageLink, RuleChainType.CORE); + } + + fetchEdgeRuleChains(pageLink: PageLink) { + return this.ruleChainService.getAutoAssignToEdgeRuleChains().pipe( + mergeMap((ruleChains) => { + this.config.componentsData.autoAssignToEdgeRuleChainIds = []; + ruleChains.map(ruleChain => this.config.componentsData.autoAssignToEdgeRuleChainIds.push(ruleChain.id.id)); + return this.ruleChainService.getRuleChains(pageLink, RuleChainType.EDGE); + }) + ); + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts index 1bc80e96db..6583d17852 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts @@ -19,6 +19,7 @@ import { Component, OnInit } from '@angular/core'; import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart'; import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { Router } from '@angular/router'; +import { RuleChainType } from '@app/shared/models/rule-chain.models'; @Component({ // tslint:disable-next-line:component-selector @@ -48,7 +49,12 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { $event.stopPropagation(); } if (node.targetRuleChainId) { - this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + if (node.ruleChainType === RuleChainType.EDGE) { + this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`); + } else { + this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + } + } } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts index f81c5d8eaa..735b56a783 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -33,11 +33,12 @@ import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundl import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { Direction } from "@shared/models/page/sort-order"; +import { Direction } from '@shared/models/page/sort-order'; +import { map } from 'rxjs/operators'; @Injectable() export class WidgetsBundlesTableConfigResolver implements Resolve> { @@ -106,7 +107,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.translate.instant('widgets-bundle.delete-widgets-bundles-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundles-text'); - this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink); + this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id); this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle); this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); @@ -119,6 +120,15 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.entitySelectionEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.detailsReadonly = (widgetsBundle) => !this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + const authState = getCurrentAuthState(this.store); + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink).pipe( + map((widgetBundles) => { + if (!authState.edgesSupportEnabled) { + widgetBundles.data = widgetBundles.data.filter(widgetBundle => widgetBundle.alias !== 'edge_widgets'); + } + return widgetBundles; + }) + ); return this.config; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 245d09ad5e..b3bbb861e2 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -162,6 +162,11 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.noEntitiesMatchingText = 'device.no-devices-matching'; this.entityRequiredText = 'device.device-required'; break; + case EntityType.EDGE: + this.entityText = 'edge.edge'; + this.noEntitiesMatchingText = 'edge.no-edges-matching'; + this.entityRequiredText = 'edge.edge-required'; + break; case EntityType.ENTITY_VIEW: this.entityText = 'entity-view.entity-view'; this.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 9d110f2897..72baf88cfe 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -60,6 +60,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @Input() entityType: EntityType; + @Input() + subType: string; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -216,8 +219,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV fetchEntities(searchText?: string): Observable>> { this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(this.entityType, searchText, - 50, '', {ignoreLoading: true}).pipe( + 50, this.subType ? this.subType : '', {ignoreLoading: true}).pipe( map((data) => data ? data : [])); } diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts index 0ec55276cf..14d2fcb228 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts @@ -27,6 +27,7 @@ import { BroadcastService } from '@app/core/services/broadcast.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { AssetService } from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service'; +import { EdgeService } from '@core/http/edge.service'; @Component({ selector: 'tb-entity-subtype-autocomplete', @@ -82,6 +83,7 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, public translate: TranslateService, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.subTypeFormGroup = this.fb.group({ @@ -115,6 +117,14 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, this.subTypes = null; }); break; + case EntityType.EDGE: + this.selectEntitySubtypeText = 'edge.select-edge-type'; + this.entitySubtypeText = 'edge.edge-type'; + this.entitySubtypeRequiredText = 'edge.edge-type-required'; + this.broadcastSubscription = this.broadcast.on('edgeSaved', () => { + this.subTypes = null; + }); + break; case EntityType.ENTITY_VIEW: this.selectEntitySubtypeText = 'entity-view.select-entity-view-type'; this.entitySubtypeText = 'entity-view.entity-view-type'; @@ -202,6 +212,9 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, case EntityType.DEVICE: subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + subTypesObservable = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index 8628f1edef..6dffa176cd 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -27,6 +27,7 @@ import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { AssetService } from '@core/http/asset.service'; import { DeviceService } from '@core/http/device.service'; +import { EdgeService } from '@core/http/edge.service'; import { EntityViewService } from '@core/http/entity-view.service'; import { BroadcastService } from '@core/services/broadcast.service'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; @@ -96,6 +97,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, public translate: TranslateService, private assetService: AssetService, private deviceService: DeviceService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.entitySubtypeListFormGroup = this.fb.group({ @@ -139,6 +141,16 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.entitySubtypes = null; }); break; + case EntityType.EDGE: + this.placeholder = this.required ? this.translate.instant('edge.enter-edge-type') + : this.translate.instant('edge.any-edge'); + this.secondaryPlaceholder = '+' + this.translate.instant('edge.edge-type'); + this.noSubtypesMathingText = 'edge.no-edge-types-matching'; + this.subtypeListEmptyText = 'edge.edge-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('edgeSaved', () => { + this.entitySubtypes = null; + }); + break; case EntityType.ENTITY_VIEW: this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type') : this.translate.instant('entity-view.any-entity-view'); @@ -260,6 +272,9 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, case EntityType.DEVICE: subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + subTypesObservable = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts index 7dc9eb1e80..c8a4f0dedf 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts @@ -25,6 +25,7 @@ import { DeviceService } from '@core/http/device.service'; import { EntitySubtype, EntityType } from '@app/shared/models/entity-type.models'; import { BroadcastService } from '@app/core/services/broadcast.service'; import { AssetService } from '@core/http/asset.service'; +import { EdgeService } from '@core/http/edge.service'; import { EntityViewService } from '@core/http/entity-view.service'; @Component({ @@ -78,6 +79,7 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni public translate: TranslateService, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.subTypeFormGroup = this.fb.group({ @@ -111,6 +113,14 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni this.subTypesOptionsSubject.next(''); }); break; + case EntityType.EDGE: + this.entitySubtypeTitle = 'edge.edge-type'; + this.entitySubtypeRequiredText = 'edge.edge-type-required'; + this.broadcastSubscription = this.broadcast.on('edgeSaved',() => { + this.subTypes = null; + this.subTypesOptionsSubject.next(''); + }); + break; case EntityType.ENTITY_VIEW: this.entitySubtypeTitle = 'entity-view.entity-view-type'; this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; @@ -208,6 +218,9 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni case EntityType.DEVICE: this.subTypes = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + this.subTypes = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: this.subTypes = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts index 9e5d986c5f..9a538b6560 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -17,7 +17,7 @@ import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable } from 'rxjs'; -import { share, tap } from 'rxjs/operators'; +import { map, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -25,6 +25,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetService } from '@core/http/widget.service'; import { isDefined } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ selector: 'tb-widgets-bundle-select', @@ -81,6 +82,13 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni ngOnInit() { this.widgetsBundles$ = this.getWidgetsBundles().pipe( + map((widgetsBundles) => { + const authState = getCurrentAuthState(this.store); + if (!authState.edgesSupportEnabled) { + widgetsBundles = widgetsBundles.filter(widgetsBundle => widgetsBundle.alias !== 'edge_widgets'); + } + return widgetsBundles; + }), tap((widgetsBundles) => { this.widgetsBundles = widgetsBundles; if (this.selectFirstBundle) { diff --git a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts index 690b3ae962..62f67836b2 100644 --- a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts @@ -18,13 +18,13 @@ import { FunctionArg, FunctionArgType, TbEditorCompletions } from '@shared/model export const entityIdHref = 'EntityId'; -export const baseDataHref = 'Base data'; +export const baseDataHref = 'Base data'; export const alarmDataHref = 'Alarm data'; -export const alarmDataQueryHref = 'Alarm data query'; +export const alarmDataQueryHref = 'Alarm data query'; -export const attributeScopeHref = 'Attribute scope'; +export const attributeScopeHref = 'Attribute scope'; export const entityTypeHref = 'EntityType'; @@ -32,15 +32,15 @@ export const pageDataHref = 'DeviceInfo'; -export const assetInfoHref = 'AssetInfo'; +export const assetInfoHref = 'AssetInfo'; export const entityViewInfoHref = 'EntityViewInfo'; export const entityRelationsQueryHref = 'EntityRelationsQuery'; -export const entityRelationInfoHref = 'EntityRelationInfo'; +export const entityRelationInfoHref = 'EntityRelationInfo'; -export const dashboardInfoHref = 'DashboardInfo'; +export const dashboardInfoHref = 'DashboardInfo'; export const deviceHref = 'Device'; @@ -50,7 +50,7 @@ export const entityViewHref = 'Entity relation'; -export const dashboardHref = 'Dashboard'; +export const dashboardHref = 'Dashboard'; export const customerHref = 'Customer'; @@ -62,7 +62,7 @@ export const aggregationTypeHref = 'Data Sort Order'; -export const userHref = 'User'; +export const userHref = 'User'; export const entityDataHref = 'Entity data'; diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index 5c02089b04..1f900d29a1 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -27,11 +27,13 @@ export enum AliasFilterType { stateEntity = 'stateEntity', assetType = 'assetType', deviceType = 'deviceType', + edgeType = 'edgeType', entityViewType = 'entityViewType', apiUsageState = 'apiUsageState', relationsQuery = 'relationsQuery', assetSearchQuery = 'assetSearchQuery', deviceSearchQuery = 'deviceSearchQuery', + edgeSearchQuery = 'edgeSearchQuery', entityViewSearchQuery = 'entityViewSearchQuery' } @@ -44,11 +46,13 @@ export const aliasFilterTypeTranslationMap = new Map( [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], + [ AliasFilterType.edgeType, 'alias.filter-type-edge-type' ], [ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ], [ AliasFilterType.apiUsageState, 'alias.filter-type-apiUsageState' ], [ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ], [ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ], [ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ], + [ AliasFilterType.edgeSearchQuery, 'alias.filter-type-edge-search-query' ], [ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ] ] ); @@ -86,6 +90,11 @@ export interface DeviceTypeFilter { deviceNameFilter?: string; } +export interface EdgeTypeFilter { + edgeType?: string; + edgeNameFilter?: string; +} + export interface EntityViewFilter { entityViewType?: string; entityViewNameFilter?: string; @@ -126,6 +135,10 @@ export interface DeviceSearchQueryFilter extends EntitySearchQueryFilter { deviceTypes?: string[]; } +export interface EdgeSearchQueryFilter extends EntitySearchQueryFilter { + edgeTypes?: string[]; +} + export interface EntityViewSearchQueryFilter extends EntitySearchQueryFilter { entityViewTypes?: string[]; } @@ -138,12 +151,14 @@ export type EntityFilters = StateEntityFilter & AssetTypeFilter & DeviceTypeFilter & + EdgeTypeFilter & EntityViewFilter & RelationsQueryFilter & AssetSearchQueryFilter & DeviceSearchQueryFilter & EntityViewSearchQueryFilter & - EntitySearchQueryFilter; + EntitySearchQueryFilter & + EdgeSearchQueryFilter; export interface EntityAliasFilter extends EntityFilters { type?: AliasFilterType; 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 a795312ff4..7c90411aba 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -55,7 +55,9 @@ export enum ActionType { PROVISION_SUCCESS = 'PROVISION_SUCCESS', PROVISION_FAILURE = 'PROVISION_FAILURE', TIMESERIES_UPDATED = 'TIMESERIES_UPDATED', - TIMESERIES_DELETED = 'TIMESERIES_DELETED' + TIMESERIES_DELETED = 'TIMESERIES_DELETED', + ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', + UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE' } export enum ActionStatus { @@ -91,7 +93,9 @@ export const actionTypeTranslations = new Map( [ActionType.PROVISION_SUCCESS, 'audit-log.type-provision-success'], [ActionType.PROVISION_FAILURE, 'audit-log.type-provision-failure'], [ActionType.TIMESERIES_UPDATED, 'audit-log.type-timeseries-updated'], - [ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'] + [ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'], + [ActionType.ASSIGNED_TO_EDGE, 'audit-log.type-assigned-to-edge'], + [ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge'] ] ); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 7b72b6a6e4..44327e1c2c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -115,6 +115,7 @@ export const HelpLinks = { users: helpBaseUrl + '/docs/user-guide/ui/users', devices: helpBaseUrl + '/docs/user-guide/ui/devices', deviceProfiles: helpBaseUrl + '/docs/user-guide/ui/device-profiles', + edges: helpBaseUrl + '/docs/user-guide/ui/edges', assets: helpBaseUrl + '/docs/user-guide/ui/assets', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', @@ -127,6 +128,8 @@ export const HelpLinks = { widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc', widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', + ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', + ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge' } }; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index f2a1f7d765..96a2a6bc00 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -175,13 +175,6 @@ export const deviceTransportTypeConfigurationInfoMap = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], - [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.X509_CERTIFICATE, 'X.509'], [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'], [DeviceCredentialsType.LWM2M_CREDENTIALS, 'LwM2M Credentials'] ] diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts new file mode 100644 index 0000000000..fd7cb57706 --- /dev/null +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2021 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 { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CustomerId } from '@shared/models/id/customer-id'; +import { EdgeId } from '@shared/models/id/edge-id'; +import { EntitySearchQuery } from '@shared/models/relation.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { BaseEventBody } from '@shared/models/event.models'; +import { EventId } from '@shared/models/id/event-id'; + +export interface Edge extends BaseData { + tenantId?: TenantId; + customerId?: CustomerId; + name: string; + type: string; + secret: string; + routingKey: string; + cloudEndpoint: string; + edgeLicenseKey: string; + label?: string; + additionalInfo?: any; + rootRuleChainId?: RuleChainId; +} + +export interface EdgeInfo extends Edge { + customerTitle: string; + customerIsPublic: boolean; +} + +export interface EdgeSearchQuery extends EntitySearchQuery { + edgeTypes: Array; +} + +export enum EdgeEventType { + DASHBOARD = "DASHBOARD", + ASSET = "ASSET", + DEVICE = "DEVICE", + DEVICE_PROFILE = "DEVICE_PROFILE", + ENTITY_VIEW = "ENTITY_VIEW", + ALARM = "ALARM", + RULE_CHAIN = "RULE_CHAIN", + RULE_CHAIN_METADATA = "RULE_CHAIN_METADATA", + EDGE = "EDGE", + USER = "USER", + CUSTOMER = "CUSTOMER", + RELATION = "RELATION", + TENANT = "TENANT", + WIDGETS_BUNDLE = "WIDGETS_BUNDLE", + WIDGET_TYPE = "WIDGET_TYPE", + ADMIN_SETTINGS = "ADMIN_SETTINGS" +} + +export enum EdgeEventActionType { + ADDED = "ADDED", + DELETED = "DELETED", + UPDATED = "UPDATED", + POST_ATTRIBUTES = "POST_ATTRIBUTES", + ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED", + ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED", + TIMESERIES_UPDATED = "TIMESERIES_UPDATED", + CREDENTIALS_UPDATED = "CREDENTIALS_UPDATED", + ASSIGNED_TO_CUSTOMER = "ASSIGNED_TO_CUSTOMER", + UNASSIGNED_FROM_CUSTOMER = "UNASSIGNED_FROM_CUSTOMER", + RELATION_ADD_OR_UPDATE = "RELATION_ADD_OR_UPDATE", + RELATION_DELETED = "RELATION_DELETED", + RPC_CALL = "RPC_CALL", + ALARM_ACK = "ALARM_ACK", + ALARM_CLEAR = "ALARM_CLEAR", + ASSIGNED_TO_EDGE = "ASSIGNED_TO_EDGE", + UNASSIGNED_FROM_EDGE = "UNASSIGNED_FROM_EDGE", + CREDENTIALS_REQUEST = "CREDENTIALS_REQUEST", + ENTITY_MERGE_REQUEST = "ENTITY_MERGE_REQUEST" +} + +export enum EdgeEventStatus { + DEPLOYED = "DEPLOYED", + PENDING = "PENDING" +} + +export const edgeEventTypeTranslations = new Map( + [ + [EdgeEventType.DASHBOARD, 'edge-event.type-dashboard'], + [EdgeEventType.ASSET, 'edge-event.type-asset'], + [EdgeEventType.DEVICE, 'edge-event.type-device'], + [EdgeEventType.DEVICE_PROFILE, 'edge-event.type-device-profile'], + [EdgeEventType.ENTITY_VIEW, 'edge-event.type-entity-view'], + [EdgeEventType.ALARM, 'edge-event.type-alarm'], + [EdgeEventType.RULE_CHAIN, 'edge-event.type-rule-chain'], + [EdgeEventType.RULE_CHAIN_METADATA, 'edge-event.type-rule-chain-metadata'], + [EdgeEventType.EDGE, 'edge-event.type-edge'], + [EdgeEventType.USER, 'edge-event.type-user'], + [EdgeEventType.CUSTOMER, 'edge-event.type-customer'], + [EdgeEventType.RELATION, 'edge-event.type-relation'], + [EdgeEventType.TENANT, 'edge-event.type-tenant'], + [EdgeEventType.WIDGETS_BUNDLE, 'edge-event.type-widgets-bundle'], + [EdgeEventType.WIDGET_TYPE, 'edge-event.type-widgets-type'], + [EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'] + ] +); + +export const edgeEventActionTypeTranslations = new Map( + [ + [EdgeEventActionType.ADDED, 'edge-event.action-type-added'], + [EdgeEventActionType.DELETED, 'edge-event.action-type-deleted'], + [EdgeEventActionType.UPDATED, 'edge-event.action-type-updated'], + [EdgeEventActionType.POST_ATTRIBUTES, 'edge-event.action-type-post-attributes'], + [EdgeEventActionType.ATTRIBUTES_UPDATED, 'edge-event.action-type-attributes-updated'], + [EdgeEventActionType.ATTRIBUTES_DELETED, 'edge-event.action-type-attributes-deleted'], + [EdgeEventActionType.TIMESERIES_UPDATED, 'edge-event.action-type-timeseries-updated'], + [EdgeEventActionType.CREDENTIALS_UPDATED, 'edge-event.action-type-credentials-updated'], + [EdgeEventActionType.ASSIGNED_TO_CUSTOMER, 'edge-event.action-type-assigned-to-customer'], + [EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, 'edge-event.action-type-unassigned-from-customer'], + [EdgeEventActionType.RELATION_ADD_OR_UPDATE, 'edge-event.action-type-relation-add-or-update'], + [EdgeEventActionType.RELATION_DELETED, 'edge-event.action-type-relation-deleted'], + [EdgeEventActionType.RPC_CALL, 'edge-event.action-type-rpc-call'], + [EdgeEventActionType.ALARM_ACK, 'edge-event.action-type-alarm-ack'], + [EdgeEventActionType.ALARM_CLEAR, 'edge-event.action-type-alarm-clear'], + [EdgeEventActionType.ASSIGNED_TO_EDGE, 'edge-event.action-type-assigned-to-edge'], + [EdgeEventActionType.UNASSIGNED_FROM_EDGE, 'edge-event.action-type-unassigned-from-edge'], + [EdgeEventActionType.CREDENTIALS_REQUEST, 'edge-event.action-type-credentials-request'], + [EdgeEventActionType.ENTITY_MERGE_REQUEST, 'edge-event.action-type-entity-merge-request'] + ] +); + +export const edgeEventStatusColor = new Map( + [ + [EdgeEventStatus.DEPLOYED, '#000000'], + [EdgeEventStatus.PENDING, '#9e9e9e'] + ] +); + +export interface EdgeEventBody extends BaseEventBody { + type: string; + action: string; + entityId: string; +} + +export interface EdgeEvent extends BaseData { + tenantId: TenantId; + entityId: string; + edgeId: EdgeId; + action: EdgeEventActionType; + type: EdgeEventType; + uid: string; + body: string; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 846aa4e6dc..33e03b388e 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -29,6 +29,7 @@ export enum EntityType { ALARM = 'ALARM', RULE_CHAIN = 'RULE_CHAIN', RULE_NODE = 'RULE_NODE', + EDGE = 'EDGE', ENTITY_VIEW = 'ENTITY_VIEW', WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGET_TYPE = 'WIDGET_TYPE', @@ -160,6 +161,20 @@ export const entityTypeTranslations = new Map { firstRuleNodeId: RuleNodeId; root: boolean; debugMode: boolean; + type: string; configuration?: any; additionalInfo?: any; + isDefault?: boolean; } export interface RuleChainMetaData { @@ -110,3 +112,8 @@ export const inputNodeComponent: RuleNodeComponentDescriptor = { name: 'Input', clazz: 'tb.internal.Input' }; + +export enum RuleChainType { + CORE = 'CORE', + EDGE = 'EDGE' +} diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 1e54baf370..73ff97b061 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -25,6 +25,7 @@ import { AfterViewInit, EventEmitter, Inject, OnInit, Directive } from '@angular import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, FormGroup } from '@angular/forms'; +import { RuleChainType } from '@shared/models/rule-chain.models'; export interface RuleNodeConfiguration { [key: string]: any; @@ -313,6 +314,7 @@ export interface FcRuleNode extends FcRuleNodeType { error?: string; highlighted?: boolean; componentClazz?: string; + ruleChainType?: RuleChainType; } export interface FcRuleEdge extends FcEdge { @@ -421,7 +423,9 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail', - 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms' + 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms', + 'org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode': 'ruleNodePushToCloud', + 'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge' }; export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string { 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 49ad67dcf1..c9520f09df 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -136,13 +136,16 @@ export enum QuickTimeInterval { DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY', THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK', PREVIOUS_WEEK = 'PREVIOUS_WEEK', + PREVIOUS_WEEK_ISO = 'PREVIOUS_WEEK_ISO', PREVIOUS_MONTH = 'PREVIOUS_MONTH', PREVIOUS_YEAR = 'PREVIOUS_YEAR', CURRENT_HOUR = 'CURRENT_HOUR', CURRENT_DAY = 'CURRENT_DAY', CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR', CURRENT_WEEK = 'CURRENT_WEEK', - CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR', + CURRENT_WEEK_ISO = 'CURRENT_WEEK_ISO', + CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_FAR', + CURRENT_WEEK_ISO_SO_FAR = 'CURRENT_WEEK_ISO_SO_FAR', CURRENT_MONTH = 'CURRENT_MONTH', CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR', CURRENT_YEAR = 'CURRENT_YEAR', @@ -154,13 +157,16 @@ export const QuickTimeIntervalTranslationMap = new Map Preisseite und wählen Sie die beste Lizenzoption für Ihre aus Fall.", + "assignedToCustomer": "Dem Kunden zugewiesen", + "edge-public": "Edge ist öffentlich", + "search": "Kanten durchsuchen", + "selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", + "any-edge": "Beliebige Kante", + "dashboard": "Kanten-Dashboard", + "sync-process-started-successfully": "Synchronisierungsprozess erfolgreich gestartet!", + "delete-edges-action-title": "Löschen { count, plural, 1 {1 Rand} other {# Rand} }", + "set-root-rule-chain-text": "Bitte wählen Sie die Regelkette zur Wurzel rule chain für die Rand", + "set-root-rule-chain-to-edges": "Regelkette zur Wurzel machen für die Rand", + "set-root-rule-chain-to-edges-text": "Die Regelkette zur Wurzel für { count, plural, 1 {1 Rand} other {# Rand} } machen", + "status": "Von Rand empfangen", + "deployed": "Bereitgestellt", + "pending": "Steht aus", + "edge-file": "Edge-Datei", + "name-starts-with": "Der Kantenname beginnt mit", + "rulechain-template": "Regelkettenvorlage", + "unassign-edges-action-title": "Heben Sie die Zuordnung von {count, plural, 1 {1 edge} other {# edge}} vom Kunden auf", + "enter-edge-type": "Geben Sie den Kantentyp ein", + "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", + "edge-type-list-empty": "Keine Kantentypen ausgewählt.", + "edge-types": "Kantentypen", + "license-key-hint": "Um Ihre Lizenz zu erhalten, navigieren Sie zur Preisseite und wählen Sie die beste Lizenzoption für Ihre aus Fall.", + "cloud-endpoint-hint": "Edge erfordert HTTP-Zugriff auf die Cloud (ThingsBoard CE / PE), um den Lizenzschlüssel zu überprüfen. Bitte geben Sie die Cloud-URL an, zu der Edge eine Verbindung herstellen kann.", + "missing-related-rule-chains-title": "In Edge fehlen verwandte Regelketten.", + "missing-related-rule-chains-text": "Randregelkette (n) zugewiesen Verwenden Sie Regelknoten, die Nachrichten an Regelkette (n) weiterleiten, die dieser Kante nicht zugeordnet sind.

Liste der fehlenden Regelketten:
{{missingRuleChains}}", + "downlinks": "Downlinks", + "no-downlinks-prompt": "Keine Downlinks gefunden", + "assigned-to-customer-widget": "Zugewiesen an: {{customerTitle}}", + "widget-datasource-error": "Dieses Widget unterstützt nur EDGE-Entitätsdatenquellen" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Es konnte keine Verbindung zum Server hergestellt werden! Bitte überprüfen Sie Ihre Internetverbindung.", "unhandled-error-code": "Unbehandelter Fehlercode: {{errorCode}}", @@ -790,6 +966,10 @@ "type-rulenodes": "Regelknoten", "list-of-rulenodes": "{ count, plural, 1 {Ein Regelknoten} other {Liste von # Regelknoten} }", "rulenode-name-starts-with": "Regelknoten beginnend mit '{{prefix}}'", + "type-edge": "Randtyp", + "type-edges": "Randtyp", + "list-of-edges": "{ count, plural, 1 {1 Rand} other {# Rand} }", + "edge-name-starts-with": "Rand beginnend mit '{{prefix}}'", "type-current-customer": "Aktueller Kunde", "search": "Entitäten suchen", "selected-entities": "{ count, plural, 1 {Entität} other {# Entitäten} } ausgewählt", @@ -862,6 +1042,14 @@ "entity-view-types": "Entitätsansichtstypen", "name": "Name", "name-required": "Name ist erforderlich.", + "assign-entity-view-to-edge": "Entitätsansicht dem Rand zuordnen", + "assign-entity-view-to-edge-text":"Bitte wählen Sie die Entitätsansicht aus, die dem Rand zugeordnet werden sollen", + "unassign-entity-view-from-edge-title": "Sind Sie sicher, dass Sie die Zuordnung für Entitätsansicht '{{entityViewName}}' aufheben möchten?", + "unassign-entity-view-from-edge-text": "Nach Bestätigung wird die Zuordnung des Entitätsansichts aufgehoben und es ist für den Kunden nicht mehr zugänglich.", + "unassign-entity-views-from-edge-action-title": "Rand { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichte} } aufheben", + "unassign-entity-view-from-edge": "Entitätsansichtzuordnung aufheben", + "unassign-entity-views-from-edge-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichte} } nicht mehr zuordnen möchten?", + "unassign-entity-views-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Entitätsansicht nicht zugewiesen und sind für den Rand nicht zugänglich.", "description": "Beschreibung", "events": "Ereignisse", "details": "Details", @@ -899,6 +1087,7 @@ "type-debug-rule-chain": "Fehlersuche", "no-events-prompt": "Keine Ereignisse gefunden", "error": "Fehler", + "type-edge-event": "Downlink", "alarm": "Alarm", "event-time": "Ereigniszeit", "server": "Server", @@ -1257,7 +1446,34 @@ "no-rulechains-matching": "Es wurden keine passenden Regelketten für '{{entity}}' gefunden.", "rulechain-required": "Regelkette ist erforderlich", "management": "Regelverwaltung", - "debug-mode": "Modus zur Fehlersuche" + "debug-mode": "Modus zur Fehlersuche", + "assign-rulechains": "Regelketten zuweisen", + "assign-new-rulechain": "Neues Regelkette zuweisen", + "delete-rulechains": "Regelketten löschen", + "unassign-rulechain": "Nicht zugeordnete Regelkette", + "unassign-rulechains": "Nicht zugeordnete Regelketten", + "unassign-rulechain-title": "Möchten Sie die Zuordnung die Regelkette '{{ruleChainTitle}}' wirklich aufheben?", + "unassign-rulechain-from-edge-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Regelkette aufgehoben und sie sind für den Rand nicht mehr zugänglich.", + "unassign-rulechains-from-edge-action-title": "Zuordnung { count, plural, 1 {1 Regelkette} other {# Regelketten} } vom Rand aufheben", + "unassign-rulechains-from-edge-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Regelketten aufgehoben und sie sind für den Rand nicht mehr zugänglich.", + "assign-rulechain-to-edge-title": "Regelkette(n) dem Rand zuordnen", + "assign-rulechain-to-edge-text": "Bitte wählen Sie die Regelketten aus, die Sie dem Rand zuordnen möchten", + "set-edge-template-root-rulechain": "Erstellen Sie den Stamm der Regelkettenkantenvorlage", + "set-edge-template-root-rulechain-title": "Möchten Sie die Kantenvorlage der Regelkette '{{ruleChainName}}' wirklich als Root festlegen?", + "set-edge-template-root-rulechain-text": "Nach der Bestätigung wird die Regelkette zum Stamm der Kantenvorlage und zur Stammregelkette für neu erstellte Kanten.", + "invalid-rulechain-type-error": "Regelkette konnte nicht importiert werden: Ungültige Regelkettentyp. Erwarteter Typ ist {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Weisen Sie bei der Erstellung den Kanten die Regelkette zu", + "set-auto-assign-to-edge-title": "Möchten Sie die Kantenregelkette '{{ruleChainName}}' bei der Erstellung automatisch den Kanten zuweisen?", + "set-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung automatisch den Kanten zugewiesen.", + "unset-auto-assign-to-edge": "Deaktiviert die Zuordnung der Regelkette zu Kanten bei der Erstellung", + "unset-auto-assign-to-edge-title": "Möchten Sie die Kantenregelkette '{{ruleChainName}}' bei der Erstellung unbedingt den Kanten zuweisen?", + "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", + "edge-template-root": "Vorlagenstamm", + "search": "Suchen Sie nach Regelketten", + "selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", + "open-rulechain": "Regelkette öffnen", + "assign-to-edge": "Rand zuweisen", + "edge-rulechain": "Kantenregelkette" }, "rulenode": { "details": "Details", 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 31f906b9c0..fa18da65d4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -311,8 +311,11 @@ "filter-type-device-type-description": "Devices of type '{{deviceType}}'", "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewType}}'", + "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewType}}' and with name starting with '{{prefix}}'", + "filter-type-edge-type": "Edge type", + "filter-type-edge-type-description": "Edges of type '{{edgeType}}'", + "filter-type-edge-type-and-name-description": "Edges of type '{{edgeType}}' and with name starting with '{{prefix}}'", "filter-type-relations-query": "Relations query", "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Asset search query", @@ -322,6 +325,8 @@ "filter-type-entity-view-search-query": "Entity view search query", "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-apiUsageState": "Api Usage State", + "filter-type-edge-search-query": "Edge search query", + "filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "entity-filter": "Entity filter", "resolve-multiple": "Resolve as multiple entities", "filter-type": "Filter type", @@ -377,6 +382,8 @@ "asset-details": "Asset details", "assign-assets": "Assign assets", "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", + "assign-asset-to-edge-title": "Assign Asset(s) To Edge", + "assign-asset-to-edge-text":"Please select the assets to assign to the edge", "delete-assets": "Delete assets", "unassign-assets": "Unassign assets", "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", @@ -395,6 +402,7 @@ "unassign-asset": "Unassign asset", "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", + "unassign-assets-from-edge": "Unassign assets from edge", "copyId": "Copy asset Id", "idCopiedMessage": "Asset Id has been copied to clipboard", "select-asset": "Select asset", @@ -403,9 +411,14 @@ "name-starts-with": "Asset name starts with", "import": "Import assets", "asset-file": "Asset file", - "search": "Search assets", - "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected", - "label": "Label" + "label": "Label", + "assign-asset-to-edge": "Assign Asset(s) To Edge", + "unassign-asset-from-edge": "Unassign asset", + "unassign-asset-from-edge-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-from-edge-text": "After the confirmation the asset will be unassigned and won't be accessible by the edge.", + "unassign-assets-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-from-edge-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the edge.", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected" }, "attribute": { "attributes": "Attributes", @@ -508,6 +521,8 @@ "type-credentials-updated": "Credentials updated", "type-assigned-to-customer": "Assigned to Customer", "type-unassigned-from-customer": "Unassigned from Customer", + "type-assigned-to-edge": "Assigned to Edge", + "type-unassigned-from-edge": "Unassigned from Edge", "type-activated": "Activated", "type-suspended": "Suspended", "type-credentials-read": "Credentials read", @@ -578,6 +593,7 @@ "public-dashboards": "Public Dashboards", "public-devices": "Public Devices", "public-assets": "Public Assets", + "public-edges": "Public Edges", "public-entity-views": "Public Entity Views", "add": "Add Customer", "delete": "Delete customer", @@ -588,6 +604,8 @@ "manage-public-dashboards": "Manage public dashboards", "manage-customer-assets": "Manage customer assets", "manage-public-assets": "Manage public assets", + "manage-customer-edges": "Manage customer edges", + "manage-public-edges": "Manage customer edges", "add-customer-text": "Add new customer", "no-customers-text": "No customers found", "customer-details": "Customer details", @@ -614,7 +632,9 @@ "default-customer": "Default customer", "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level", "search": "Search customers", - "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected" + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected", + "edges": "Customer edge instances", + "manage-edges": "Manage edges" }, "datetime": { "date-from": "Date from", @@ -630,6 +650,7 @@ "add": "Add Dashboard", "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer", "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer", + "assign-dashboard-to-edge-title": "Assign Dashboard(s) To Edge", "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", "assign-to-customer": "Assign to customer", "unassign-from-customer": "Unassign from customer", @@ -773,7 +794,12 @@ "search": "Search dashboards", "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected", "home-dashboard": "Home dashboard", - "home-dashboard-hide-toolbar": "Hide home dashboard toolbar" + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar", + "unassign-dashboard-from-edge-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the edge.", + "unassign-dashboards-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-from-edge-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the edge.", + "assign-dashboard-to-edge": "Assign Dashboard(s) To Edge", + "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge" }, "datakey": { "settings": "Settings", @@ -846,6 +872,8 @@ "assign-to-customer": "Assign to customer", "assign-device-to-customer": "Assign Device(s) To Customer", "assign-device-to-customer-text": "Please select the devices to assign to the customer", + "assign-device-to-edge-title": "Assign Device(s) To Edge", + "assign-device-to-edge-text":"Please select the devices to assign to the edge", "make-public": "Make device public", "make-private": "Make device private", "no-devices-text": "No devices found", @@ -861,6 +889,9 @@ "unassign-from-customer": "Unassign from customer", "unassign-devices": "Unassign devices", "unassign-devices-action-title": "Unassign { count, plural, 1 {1 device} other {# devices} } from customer", + "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", + "unassign-devices-from-edge": "Unassign devices from edge", "assign-new-device": "Assign new device", "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", @@ -888,11 +919,11 @@ "lwm2m-key-required": "LwM2M Security config key is required.", "lwm2m-value": "LwM2M Security config", "lwm2m-value-required": "LwM2M Security config value is required.", - "lwm2m-value-json-error": "LwM2M Security config value is not json format.", + "lwm2m-value-format-error": "Security config value must be in LwM2M Security config format.", "lwm2m-endpoint": "Client endpoint/identity", "lwm2m-security-info": "Security Config Info", "lwm2m-value-edit": "Edit Security config", - "lwm2m-value-edit-tip": "Edit security config json editor", + "lwm2m-credentials-value-tip": "Edit security config json editor", "lwm2m-security-config": { "identity": "Client Identity", "client-key": "Client Key", @@ -959,7 +990,9 @@ "specific-configuration": "Specific configuration", "customer-to-assign-device": "Customer to assign the device", "add-credential": "Add credential" - } + }, + "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge." }, "device-profile": { "device-profile": "Device profile", @@ -1149,17 +1182,32 @@ "instances-list": "Instances list", "instances-input": "Input Instance Id value", "instances-input-holder": "Input Instance number...", - "resource-label": "Resource", + "instance-label": "Instance", + "resource-label": " Resource name", "observe-label": "Observe", "attribute-label": "Attribute", "telemetry-label": "Telemetry", "key-name-label": "Key Name", + "attribute-lwm2m-label": "AttrLwm2m", + "resource-tip": "ID & Original Name of the Resource", "is-observe-tip": "Is Observe", + "not-observe-tip": "To observe select telemetry or attributes first", "is-attr-tip": "Is Attribute", "is-telemetry-tip": "Is Telemetry", - "key-name-tip": "Key Name", + "key-name-tip": "Key Name in Camel format", + "attribute-lwm2m-tip": "Attributes Lwm2m", + "attribute-lwm2m-disable-tip": "To edit Attributes Lwm2m select telemetry or attributes first", + "valid-attribute-lwm2m-key": "Name have be only '{{attrEnums}}'", + "valid-attribute-lwm2m-value": "Value have be Long, Double and greater than zero or null/empty", + "no-data": "No attributes", "key-name": "Key Name", - "key-name_label": "Key Name in Camel format", + "attribute-lwm2m-name": "Name attribute", + "attribute-lwm2m-value": "Value", + "attribute-lwm2m-toolbar-edit": "Edit attributes Lwm2m", + "attribute-lwm2m-toolbar-view": "View Attributes Lwm2m", + "attribute-lwm2m-destination": "Destination:", + "attribute-lwm2m-add-tip": "Add attribute lwm2m", + "attribute-lwm2m-remove-tip": "Remove attribute lwm2m", "required": " value is required.", "mode": "Security config mode", "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }", @@ -1193,6 +1241,144 @@ "column": "Column", "row": "Row" }, + "edge": { + "edge": "Edge", + "edge-instances": "Edge instances", + "edge-file": "Edge file", + "management": "Edge management", + "no-edges-matching": "No edges matching '{{entity}}' were found.", + "rulechain-templates": "Rule chain templates", + "rulechains": "Rule chains", + "edge-rulechains": "Edge Rule chains", + "add": "Add Edge", + "no-edges-text": "No edges found", + "edge-details": "Edge details", + "add-edge-text": "Add new edge", + "delete": "Delete edge", + "delete-edges": "Delete edges", + "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", + "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", + "delete-edges-title": "Are you sure you want to edge { count, plural, 1 {1 edge} other {# edges} }?", + "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", + "name": "Name", + "name-starts-with": "Edge name starts with", + "name-required": "Name is required.", + "edge-license-key": "Edge License Key", + "edge-license-key-required": "Edge License Key is required.", + "edge-license-key-hint": "To obtain your license please navigate to the pricing page and select the best license option for your case.", + "cloud-endpoint": "Cloud Endpoint", + "cloud-endpoint-required": "Cloud Endpoint is required.", + "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", + "description": "Description", + "details": "Details", + "events": "Events", + "copy-id": "Copy Edge Id", + "id-copied-message": "Edge Id has been copied to clipboard", + "sync": "Sync Edge", + "sync-message": "Edge has been synchronized", + "edge-required": "Edge required", + "edge-type": "Edge type", + "edge-type-required": "Edge type is required.", + "event-action": "Event action", + "entity-id": "Entity ID", + "select-edge-type": "Select edge type", + "assign-to-customer": "Assign to customer", + "assign-to-customer-text": "Please select the customer to assign the edge(s)", + "assign-edge-to-customer": "Assign Edge(s) To Customer", + "assign-edge-to-customer-text": "Please select the edges to assign to the customer", + "assignedToCustomer": "Assigned to customer", + "edge-public": "Edge is public", + "assigned-to-customer": "Assigned to: {{customerTitle}}", + "unassign-from-customer": "Unassign from customer", + "assign-edges-text": "Assign { count, plural, 1 {1 edge} other {# edges} } to customer", + "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", + "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", + "unassign-edges-title": "Are you sure you want to unassign { count, plural, 1 {1 edge} other {# edges} }?", + "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", + "make-public": "Make edge public", + "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", + "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", + "make-private": "Make edge private", + "public": "Public", + "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", + "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", + "import": "Import edge", + "label": "Label", + "load-entity-error": "Failed to load data. Entity not found or has been deleted.", + "assign-new-edge": "Assign new edge", + "manage-edge-dashboards": "Edge dashboards", + "unassign-from-edge": "Unassign from edge", + "dashboards": "Edge Dashboards", + "manage-edge-rulechains": "Edge rule chains", + "rulechain-template": "Rule chain template", + "edge-key": "Edge key", + "copy-edge-key": "Copy Edge key", + "edge-key-copied-message": "Edge key has been copied to clipboard", + "edge-secret": "Edge secret", + "copy-edge-secret": "Copy Edge secret", + "edge-secret-copied-message": "Edge secret has been copied to clipboard", + "manage-edge-assets": "Edge assets", + "manage-edge-devices": "Edge devices", + "manage-edge-entity-views": "Edge entity views", + "assets": "Edge assets", + "devices": "Edge devices", + "entity-views": "Edge entity views", + "set-root-rule-chain-text": "Please select root rule chain for edge(s)", + "set-root-rule-chain-to-edges": "Set root rule chain for Edge(s)", + "set-root-rule-chain-to-edges-text": "Set root rule chain for { count, plural, 1 {1 edge} other {# edges} }", + "search": "Search edges", + "selected-edges": "{ count, plural, 1 {1 edge} other {# edges} } selected", + "any-edge": "Any edge", + "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", + "edge-type-list-empty": "No edge types selected.", + "edge-types": "Edge types", + "dashboard": "Edge dashboard", + "enter-edge-type": "Enter edge type", + "deployed": "Deployed", + "pending": "Pending", + "downlinks": "Downlinks", + "no-downlinks-prompt": "No downlinks found", + "sync-process-started-successfully": "Sync process started successfully!", + "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", + "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", + "widget-datasource-error": "This widget supports only EDGE entity datasource" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alar", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-user": "User", + "type-customer": "Customer", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", "unhandled-error-code": "Unhandled error code: {{errorCode}}", @@ -1307,7 +1493,11 @@ "no-entities-prompt": "No entities found", "no-data": "No data to display", "columns-to-display": "Columns to Display", - "type-api-usage-state": "Api Usage State" + "type-api-usage-state": "Api Usage State", + "type-edge": "Edge", + "type-edges": "Edges", + "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", + "edge-name-starts-with": "Edges whose names start with '{{prefix}}'" }, "entity-field": { "created-time": "Created time", @@ -1359,6 +1549,7 @@ "assign-to-customer": "Assign to customer", "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", + "assign-entity-view-to-edge-title": "Assign Entity View(s) To Edge", "no-entity-views-text": "No entity views found", "assign-to-customer-text": "Please select the customer to assign the entity view(s)", "entity-view-details": "Entity view details", @@ -1425,8 +1616,14 @@ "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others.", - "search": "Search entity views", - "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } selected" + "assign-entity-view-to-edge": "Assign Entity View(s) To Edge", + "assign-entity-view-to-edge-text":"Please select the entity views to assign to the edge", + "unassign-entity-view-from-edge-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "After the confirmation the entity view will be unassigned and won't be accessible by the edge.", + "unassign-entity-views-from-edge-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from edge", + "unassign-entity-view-from-edge": "Unassign entity view", + "unassign-entity-views-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?", + "unassign-entity-views-from-edge-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the edge." }, "event": { "event-type": "Event type", @@ -1837,7 +2034,11 @@ "access-token": "Access token", "isgateway": "Is Gateway", "activity-time-from-gateway-device": "Activity time from gateway device", - "description": "Description" + "description": "Description", + "edge-license-key": "License Key", + "cloud-endpoint": "Cloud Endpoint", + "routing-key": "Edge key", + "secret": "Edge secret" }, "stepper-text":{ "select-file": "Select a file", @@ -2037,7 +2238,28 @@ "debug-mode": "Debug mode", "search": "Search rule chains", "selected-rulechains": "{ count, plural, 1 {1 rule chain} other {# rule chains} } selected", - "open-rulechain": "Open rule chain" + "open-rulechain": "Open rule chain", + "assign-new-rulechain": "Assign new rulechain", + "edge-template-root": "Template Root", + "assign-to-edge": "Assign to Edge", + "edge-rulechain": "Edge Rule chain", + "unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.", + "unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 rulechain} other {# rulechains} }?", + "unassign-rulechains-from-edge-text": "After the confirmation all selected rulechains will be unassigned and won't be accessible by the edge.", + "assign-rulechain-to-edge-title": "Assign Rule Chain(s) To Edge", + "assign-rulechain-to-edge-text": "Please select the rulechains to assign to the edge", + "set-edge-template-root-rulechain": "Make rule chain as edge template root", + "set-edge-template-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' edge template root?", + "set-edge-template-root-rulechain-text": "After the confirmation the rule chain will become edge template root and will be root rule chain for a newly created edges.", + "invalid-rulechain-type-error": "Unable to import rule chain: Invalid rule chain type. Expected type is {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Assign rule chain to edge(s) on creation", + "set-auto-assign-to-edge-title": "Are you sure you want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "set-auto-assign-to-edge-text": "After the confirmation the edge rule chain will be automatically assigned to edge(s) on creation.", + "unset-auto-assign-to-edge": "Do not assign rule chain to edge(s) on creation", + "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", + "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", + "unassign-rulechains": "Unassign rulechains" }, "rulenode": { "details": "Details", @@ -2243,14 +2465,17 @@ "yesterday": "Yesterday", "day-before-yesterday": "Day before yesterday", "this-day-last-week": "This day last week", - "previous-week": "Previous week", + "previous-week": "Previous week (Sun - Sat)", + "previous-week-iso": "Previous week (Mon - Sun)", "previous-month": "Previous month", "previous-year": "Previous year", "current-hour": "Current hour", "current-day": "Current day", "current-day-so-far": "Current day so far", - "current-week": "Current week", - "current-week-so-far": "Current week so far", + "current-week": "Current week (Sun - Sat)", + "current-week-iso": "Current week (Mon - Sun)", + "current-week-so-far": "Current week so far (Sun - Sat)", + "current-week-iso-so-far": "Current week so far (Mon - Sun)", "current-month": "Current month", "current-month-so-far": "Current month so far", "current-year": "Current year", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 37e6ce3931..ba278e796e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -309,6 +309,8 @@ "filter-type-entity-view-type": "Tipo de vista de entidad", "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityView}}'", "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityView}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-edge-type": "Tipo de borde", + "filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'", "filter-type-relations-query": "Consulta de relaciones", "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Búsqueda de activos", @@ -318,11 +320,17 @@ "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad", "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}", "filter-type-apiUsageState": "Uso de API", + "filter-type-edge-search-query": "Consultar búsqueda de borde", + "filter-type-edge-search-query-description": "Bordes con tipos {{edgeTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "type-assigned-to-edge": "Asignado a borde", + "type-unassigned-from-edge": "Sin asignar desde bordes", "entity-filter": "Filtro por entidad", "resolve-multiple": "Tomar como múltiples entidades", "filter-type": "Filtro por tipo", "filter-type-required": "Se requiere filtro por tipo.", - "entity-filter-no-entity-matched": "No se han encontrado entidades con el filtro especificado.", + "filter-type-edge-type": "Tipo de borde", + "filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'", + "entity-filter-no-entity-matched": "No se han encontrado entidades con el filtro especificado.", "no-entity-filter-specified": "No hay filtro de entidades especificado", "root-state-entity": "Usar estado de panel como raíz", "last-level-relation": "Buscar sólo la relación de último nivel", @@ -401,7 +409,15 @@ "asset-file": "Archivo del activo", "search": "Buscar activos", "selected-assets": "{ count, plural, 1 {1 activo} other {# activos} } seleccionados", - "label": "Etiqueta" + "label": "Etiqueta", + "assign-asset-to-edge": "Asignar activo(s) al borde", + "assign-asset-to-edge-text":"Por favor, seleccione los activos para asignar al borde", + "unassign-asset-from-edge": "Anular activo de bodre", + "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", + "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", + "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde", + "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", + "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." }, "attribute": { "attributes": "Atributos", @@ -575,6 +591,7 @@ "public-devices": "Dispositivos Públicos", "public-assets": "Activos Públicos", "public-entity-views": "Vistas de Entidad Públicas", + "public-edges": "Bordes públicos", "add": "Agregar cliente", "delete": "Borrar cliente", "manage-customer-users": "Gestionar usuarios del cliente", @@ -583,6 +600,8 @@ "manage-public-devices": "Gestionar dispositivos públicos", "manage-public-dashboards": "Gestionar paneles públicos", "manage-customer-assets": "Gestionar activos del cliente", + "manage-customer-edges": "Administrar bordes de clientes", + "manage-public-edges": "Administrar bordes públicos", "manage-public-assets": "Gestionar activos públicos", "add-customer-text": "Agregar nuevo cliente", "no-customers-text": "No se encontraron clientes", @@ -601,6 +620,7 @@ "description": "Descripción", "details": "Detalles", "events": "Eventos", + "edges": "Bordes del cliente", "copyId": "Copiar ID de cliente", "idCopiedMessage": "El ID de cliente se ha copiado al portapapeles", "select-customer": "Seleccionar Cliente", @@ -764,7 +784,11 @@ "select-state": "Seleccionar estado destino (target state)", "state-controller": "Controlador de estados", "search": "Buscar paneles", - "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados" + "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados", + "unassign-dashboard-from-edge-text": "Después de la confirmación, el tablero no será asignado y el borde no podrá acceder a él", + "unassign-dashboards-from-edge-text": "Después de la confirmación, se anulará la asignación de todos los paneles seleccionados y no serán accesibles por de borde", + "assign-dashboard-to-edge": "Asignar panel(es) al borde", + "assign-dashboard-to-edge-text": "Por favor selecciona los paneles para asignar al borde" }, "datakey": { "settings": "Ajustes", @@ -924,7 +948,12 @@ "specific-configuration": "Configuración específica", "customer-to-assign-device": "Cliente al que asignar el dispositivo", "add-credential": "Añadir credencial" - } + }, + "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", + "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", + "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", + "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", + "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." }, "device-profile": { "device-profile": "Perfil de dispositivo", @@ -1083,6 +1112,153 @@ "column": "Columna", "row": "Fila" }, + "edge": { + "edge": "Borde", + "edge-instances": "Instancias de Borde", + "management": "Gestión de bordes", + "no-edges-matching": "No se encontraron bordes que coincidan con '{{entity}}'", + "rulechain-templates": "Plantillas, de cadena de reglas", + "add": "Agregar borde", + "no-edges-text": "No se encontraron bordes", + "edge-details": "Detalles del borde", + "add-edge-text": "Agregar nuevo borde", + "delete": "Eliminar borde", + "delete-edges": "Eliminar bordes", + "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", + "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", + "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", + "name": "Nombre", + "name-required": "Se requiere nombre", + "edge-license-key": "Edge Clave de licencia", + "edge-license-key-required": "Se requiere edge clave de licencia", + "cloud-endpoint": "Punto final de la nube", + "cloud-endpoint-required": "Se requiere punto final de la nube", + "description": "Descripción", + "events": "Eventos", + "details": "Detalles", + "copy-id": "Copiar ID de borde", + "id-copied-message": "El ID de borde se ha copiado al portapapeles", + "sync": "Sinc Edge", + "sync-message": "Edge se ha sincronizado", + "edge-required": "Edge required", + "edge-type": "Type de la bordure", + "edge-type-required": "El tipo de borde es requerido.", + "select-edge-type": "Seleccionar tipo de borde", + "assign-to-customer": "Asignar al cliente", + "assign-to-customer-text": "Seleccione el cliente para asignar los bordes", + "assign-edge-to-customer": "Asignar borde(s) al cliente", + "assign-edge-to-customer-text": "Seleccione los bordes para asignar al cliente", + "assigned-to-customer": "Asignado al cliente", + "unassign-from-customer": "Anular asignación del cliente", + "assign-edges-text": "Asignar {cuenta, plural, 1 {1 borde} other {# bordes}} al cliente", + "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", + "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", + "make-public": "Hacer público el borde", + "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", + "make-public-edge-text": "Después de la confirmación, el borde y todos sus datos serán públicos y accesibles para otros", + "make-private": "Hacer que edge sea privado", + "public": "Public", + "make-private-edge-title": "¿Está seguro de que desea que el borde '{{edgeName}}' sea privado?", + "make-private-edge-text": "Después de la confirmación, el borde y todos sus datos se harán privados y otros no podrán acceder a ellos", + "import": "Importar borde", + "label": "Etiqueta", + "assign-new-edge": "Asignar nuevo borde", + "manage-edge-dashboards": "Administrar paneles de borde", + "unassign-from-edge": "Anular asignación de borde", + "dashboards": "Paneles de borde", + "manage-edge-rulechains": "Administrar cadenas de reglas de borde", + "rulechains": "Cadenas de regla de borde", + "edge-key": "Clave de borde", + "copy-edge-key": "Copiar clave de borde", + "edge-key-copied-message": "La clave de borde se ha copiado al portapapeles", + "edge-secret": "Borde secreto", + "copy-edge-secret": "Copiar borde secreto", + "edge-secret-copied-message": "El secreto de borde se ha copiado al portapapeles", + "manage-edge-assets": "Gestionar activos de bordes", + "manage-edge-devices": "Gestionar dispositivos de borde", + "manage-edge-entity-views": "Gestionar vistas de entidad de borde", + "assets": "Activos de borde", + "devices": "Dispositivos de borde", + "entity-views": "Vistas de entidad de borde", + "entity-id": "ID de entidad", + "event-action": "Información de la entidad", + "load-entity-error": "Entidad no encontrada. No se pudo cargar la información", + "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", + "edge-rulechains": "Cadenas de reglas de borde", + "edge-license-key-hint": "Para obtener su licencia, vaya a la página de precios y seleccione la mejor opción de licencia para su caso.", + "assignedToCustomer": "Asignada a la cliente", + "edge-public": "Edge es pública", + "set-root-rule-chain-text": "Seleccione la cadena de reglas raíz para los bordes", + "set-root-rule-chain-to-edges": "Establecer cadena de reglas raíz para Edge (s)", + "set-root-rule-chain-to-edges-text": "Establecer cadena de reglas raíz para {count, plural, 1 {1 edge} other {# ends}}", + "search": "Bordes de búsqueda", + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", + "any-edge": "Cualquier bordee", + "dashboard": "Panel de control Edge", + "deployed": "Desplegada", + "pending": "Pending", + "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", + "edge-file": "Archivo de borde", + "name-starts-with": "Edge name starts with", + "rulechain-template": "Plantilla de cadena de reglas", + "unassign-edges-action-title": "Anular la asignación de {count, plural, 1 {1 borde} other {# bordes}} del cliente", + "enter-edge-type": "Ingrese el tipo de borde", + "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", + "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", + "edge-types": "Tipos de bordes", + "license-key-hint": "Para obtener su licencia, vaya a la página de precios y seleccione la mejor opción de licencia para su caso.", + "cloud-endpoint-hint": "Edge requiere acceso HTTP (s) a la nube (ThingsBoard CE / PE) para verificar la clave de licencia. Especifique la URL de la nube a la que Edge puede conectarse.", + "missing-related-rule-chains-title": "Al borde le faltan cadenas de reglas relacionadas", + "missing-related-rule-chains-text": "Asignado a la (s) cadena (s) de reglas de borde usa nodos de reglas que reenvían mensajes a cadenas de reglas que no están asignadas a este borde.

Lista de cadenas de reglas faltantes:
{{missingRuleChains}}", + "downlinks": "Enlaces descendentes", + "no-downlinks-prompt": "No se encontraron enlaces descendentes", + "assigned-to-customer-widget": "Asignado a: {{customerTitle}}", + "widget-datasource-error": "Este widget solo admite la fuente de datos de la entidad EDGE" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Imposible conectar con el servidor! Por favor, revise su conexión a internet.", "unhandled-error-code": "Código de error no controlado: {{errorCode}}", @@ -1196,7 +1372,11 @@ "no-entities-prompt": "No se han encontrado entidades", "no-data": "No hay datos que mostrar", "columns-to-display": "Columnas a Mostrar", - "type-api-usage-state": "Estado de uso de la API" + "type-api-usage-state": "Estado de uso de la API", + "type-edge": "Borde", + "type-edges": "Bordes", + "list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", + "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" }, "entity-field": { "created-time": "Hora de creación", @@ -1315,7 +1495,15 @@ "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", "make-private-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros.", "search": "Buscar vistas de entidad", - "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas" + "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas", + "assign-entity-view-to-edge": "Asignar vista (s) de entidad a borde", + "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", + "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", + "unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", + "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", + "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", + "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" }, "event": { "event-type": "Tipo de evento", @@ -1326,6 +1514,7 @@ "type-debug-rule-chain": "Debug", "no-events-prompt": "Ningún evento encontrado.", "error": "Error", + "type-edge-event": "Downlink", "alarm": "Alarma", "event-time": "Hora del evento", "server": "Servidor", @@ -1897,7 +2086,34 @@ "debug-mode": "Modo Debug", "search": "Buscar cadenas de reglas", "selected-rulechains": "{ count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } seleccionadas", - "open-rulechain": "Abrir cadena de reglas" + "open-rulechain": "Abrir cadena de reglas", + "assign-rulechains": "Asignar cadenas de reglas", + "assign-new-rulechain": "Asignar nueva cadena de reglas", + "delete-rulechains": "Eliminar cadenas de reglas", + "unassign-rulechain": "Anular asignación de cadena de reglas", + "unassign-rulechains": "Anular asignación de cadenas de reglas", + "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", + "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", + "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", + "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", + "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", + "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", + "set-edge-template-root-rulechain": "Hacer raíz de plantilla de borde de cadena de reglas", + "set-edge-template-root-rulechain-title": "¿Está seguro de que desea que la cadena de reglas '{{ruleChainName}}' sea la raíz de la plantilla de borde?", + "set-edge-template-root-rulechain-text": "Después de la confirmación, la cadena de reglas se convertirá en la raíz de la plantilla de borde y será la cadena de reglas raíz para los bordes recién creados.", + "invalid-rulechain-type-error": "No se puede importar la cadena de reglas: Tipo de cadena de reglas no válido. El tipo esperado es {{expectedRuleChainType}}", + "set-auto-assign-to-edge": "Asignar cadena de reglas a los bordes en la creación", + "set-auto-assign-to-edge-title": "¿Está seguro de que desea asignar automáticamente la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", + "set-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde se asignará automáticamente a los bordes en la creación.", + "unset-auto-assign-to-edge": "Desmarcar asignar cadena de reglas a los bordes en la creación", + "unset-auto-assign-to-edge-title": "¿Está seguro de que desea anular la asignación de la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", + "unset-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde ya no se asignará automáticamente a los bordes en la creación.", + "edge-template-root": "Raíz de plantilla", + "search": "Cadenas de reglas de búsqueda", + "selected-rulechains": "{count, plural, 1 {1 cadena de reglas} otras {# cadenas de reglas}} seleccionadas", + "open-rulechain": "Cadena de reglas abierta", + "assign-to-edge": "Asignar a Edge", + "edge-rulechain": "Cadena de regla de borde" }, "rulenode": { "details": "Detalles", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 8ab40b2cfe..a5c25909b0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -190,9 +190,13 @@ "filter-type-entity-name": "Nom d'entité", "filter-type-entity-view-search-query": "Requête de recherche vue d'entité", "filter-type-entity-view-search-query-description": "Vues d'entité avec les types {{entityViewTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Requête de recherche de bordure", + "filter-type-edge-search-query-description": "Bordures de types {{edgeTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-entity-view-type": "Type de vue d'entité", "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityView}}' et dont le nom commence par '{{prefix}}'", "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityView}}'", + "filter-type-edge-type": "Type de la bordure", + "filter-type-edge-type-description": "Dispositifs de type '{{edgeType}}'", "filter-type-relations-query": "Interrogation des relations", "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-required": "Le type de filtre est requis.", @@ -273,7 +277,15 @@ "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets} }?", "unassign-from-customer": "Retirer du client", "view-assets": "Afficher les actifs", - "label": "Label" + "label": "Label", + "assign-asset-to-edge": "Attribuer des actifs a la bordure", + "assign-asset-to-edge-text": "Veuillez sélectionner les actifs à attribuer a la bordure", + "unassign-asset-from-edge": "Retirer de la bordure", + "unassign-asset-from-edge-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'actif '{{assetName}}'?", + "unassign-asset-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-assets-from-edge-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} de la bordure", + "unassign-assets-from-edge-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", + "unassign-assets-from-edge-text": "Après la confirmation, tous les actifs sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure." }, "attribute": { "add": "Ajouter un attribut", @@ -324,6 +336,8 @@ "type-alarm-ack": "Acquitté", "type-alarm-clear": "Effacé", "type-assigned-to-customer": "Attribué au client", + "type-assigned-to-edge": "Attribué a la bordure", + "type-unassigned-from-edge": "Non attribué de la bordure", "type-attributes-deleted": "Attributs supprimés", "type-attributes-read": "Attributs lus", "type-attributes-updated": "Attributs mis à jour", @@ -407,11 +421,13 @@ "description": "Description", "details": "Détails", "devices": "Dispositifs du client", + "edges": "Bordures du client", "entity-views": "Vues de l'entité client", "events": "Événements", "idCopiedMessage": "L'Id du client a été copié dans le presse-papier", "manage-assets": "Gérer les actifs", "manage-customer-assets": "Gérer les actifs du client", + "manage-customer-edges": "Gérer les bordures du client", "manage-customer-dashboards": "Gérer les tableaux de bord du client", "manage-customer-devices": "Gérer les dispositifs du client", "manage-customer-users": "Gérer les utilisateurs du client", @@ -420,6 +436,7 @@ "manage-public-assets": "Gérer les actifs publics", "manage-public-dashboards": "Gérer les tableaux de bord publics", "manage-public-devices": "Gérer les dispositifs publics", + "manage-public-edges": "Gérer les bordures publics", "manage-users": "Gérer les utilisateurs", "management": "Gestion des clients", "no-customers-matching": "Aucun client correspondant à '{{entity}} n'a été trouvé.", @@ -428,6 +445,7 @@ "public-dashboards": "Tableaux de bord publics", "public-devices": "Dispositifs publics", "public-entity-views": "Vues d'entités publiques", + "public-edges": "Bordures publics", "select-customer": "Sélectionner un client", "select-default-customer": "Sélectionnez le client par défaut", "title": "Titre", @@ -572,7 +590,11 @@ "view-dashboards": "Afficher les tableaux de bord", "widget-file": "Fichier du Widget", "widget-import-missing-aliases-title": "Configurer les alias utilisés par le widget importé", - "widgets-margins": "Marge entre les widgets" + "widgets-margins": "Marge entre les widgets", + "unassign-dashboard-from-edge-text": "Après la confirmation, tableau de bord sera non attribué et ne sera pas accessible a la bordure.", + "unassign-dashboards-from-edge-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", + "assign-dashboard-to-edge": "Attribuer des tableaux de bord a la bordure", + "assign-dashboard-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les tableaux de bord" }, "datakey": { "advanced": "Avancé", @@ -710,11 +732,159 @@ "unassign-from-customer": "Retirer du client", "use-device-name-filter": "Utiliser le filtre", "view-credentials": "Afficher les informations d'identification", - "view-devices": "Afficher les dispositifs" + "view-devices": "Afficher les dispositifs", + "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", + "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", + "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." }, "dialog": { "close": "Fermer le dialogue" }, + "edge": { + "edge": "Bordure", + "edge-instances": "Instances de Bord", + "management": "Gestion des bordures", + "no-edges-matching": "Aucun bordure correspondant à {{entity}} n'a été trouvé.", + "rulechain-templates": "Modèles de chaîne de règles", + "add": "Ajouter un bordure", + "no-edges-text": "Aucun bordure trouvé", + "edge-details": "Détails de la bordure", + "add-edge-text": "Ajouter une nouveau bordure", + "delete": "Supprimer la bordure", + "delete-edges": "Supprimer les bordures", + "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", + "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", + "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", + "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "name": "Nom", + "name-required": "Le nom de la bordure est requis", + "edge-license-key": "Edge Clé de licence", + "edge-license-key-required": "La edge clé de licence est requise", + "cloud-endpoint": "Clé de licence", + "cloud-endpoint-required": "La clé de licence est requise", + "description": "Dispositifs", + "events": "Événements", + "details": "Détails de l'entité", + "copy-id": "Copier borudre Id", + "id-copied-message": "Id de la bordure a été copié dans le presse-papier", + "sync": "Sync Edge", + "sync-message": "Edge a été synchronisé", + "edge-required": "Bordure est requise", + "edge-type": "Type de la bordure", + "edge-type-required": "Type de la bordure est requise.", + "select-edge-type": "Selectionner un type de la bordure", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "assign-edge-to-customer": "Attribuer la bordure au client", + "assign-edge-to-customer-text": "Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "assigned-to-customer": "Attribué au client", + "unassign-from-customer": "Retirer du client", + "assign-edges-text": "Attribuer {count, plural, 1 {1 bordure} other {# bordures}} au client", + "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", + "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", + "make-public": "Make edge public", + "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", + "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", + "make-private": "Rendre public Edge", + "public": "Public", + "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", + "make-private-edge-text": "Après la confirmation, la bordure et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres", + "import": "Importer bordure", + "label": "Etiquette", + "assign-new-edge": "Attribuer un nouvel bordure", + "manage-edge-dashboards": "Gérer les tableaux de bord", + "unassign-from-edge": "Retirer de la bordure", + "dashboards": "Tableau de bord de la bordure", + "manage-edge-rulechains": "Gérer les chaînes de règles", + "rulechains": "Chaînes de règles de la bordure", + "edge-key": "Clé de la bordure", + "copy-edge-key": "Copier clé de la bordure", + "edge-key-copied-message": "Clé de la bordure a été copié dans le presse-papier", + "edge-secret": "Secret de la bordure", + "copy-edge-secret": "Copier secret de la bordure", + "edge-secret-copied-message": "Secret de la bordure a été copié dans le presse-papier", + "manage-edge-assets": "Gérer les actifs de la bordure", + "manage-edge-devices": "Gérer les dispositifs de la bordure", + "manage-edge-entity-views": "Vues de l'entité vues de l'entité", + "assets": "Actifs de la bordure", + "devices": "Dispositifs de la bordure", + "entity-views": "Vues de l'entité bordure", + "entity-id": "ID d'entité", + "event-action": "Action d'événement", + "load-entity-error": "Entité introuvable. Échec du chargement des informations", + "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", + "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", + "edge-file": "Fichier Edge", + "edge-rulechains": "Chaînes de règles Edge", + "name-starts-with": "Le nom du bord commence par", + "edge-license-key-hint": "Pour obtenir votre licence, accédez à la page de tarification and select the best license option for your case.", + "cloud-endpoint-hint": "Edge nécessite un accès HTTP (s) au Cloud (ThingsBoard CE / PE) pour vérifier la clé de licence. Veuillez spécifier l'URL du cloud à laquelle Edge peut se connecter.", + "assignedToCustomer": "Attribué au client", + "edge-public": "Edge est public", + "set-root-rule-chain-text": "Veuillez sélectionner la chaîne de règles racine pour les arêtes", + "set-root-rule-chain-to-edges": "Définir la chaîne de règles racine pour Edge (s)", + "set-root-rule-chain-to-edges-text": "Définir la chaîne de règles racine pour {count, plural, 1 {1 edge} other {# edges}}", + "search": "Rechercher les bords", + "selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", + "any-edge": "Tout bord", + "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", + "edge-type-list-empty": "Aucun type d'arête sélectionné.", + "edge-types": "Types de bords", + "dashboard": "Tableau de bord Edge", + "enter-edge-type": "Entrez le type d'arête", + "deployed": "Déployé", + "pending": "En attente", + "downlinks": "Liens descendants", + "no-downlinks-prompt": "Aucun lien descendant trouvé", + "sync-process-started-successfully": "Le processus de synchronisation a démarré avec succès!", + "missing-related-rule-chains-title": "Edge n'a pas de chaîne (s) de règles associées", + "missing-related-rule-chains-text": "Les chaînes de règles affectées aux tronçons utilisent des nœuds de règles qui transfèrent les messages vers les chaînes de règles non affectées à ce tronçon.

Liste des chaînes de règles manquantes:
{{missingRuleChains}}", + "widget-datasource-error": "Ce widget prend en charge uniquement la source de données d'entité EDGE" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "entity": { "add-alias": "Ajouter un alias d'entité", "alarm-name-starts-with": "Les actifs dont le nom commence par '{{prefix}}'", @@ -775,6 +945,10 @@ "rule-name-starts-with": "Régles dont les noms commencent par '{{prefix}}'", "rulechain-name-starts-with": "Chaînes de régles dont les noms commencent par '{{prefix}}'", "rulenode-name-starts-with": "Les noeuds de régles dont le nom commence par '{{prefix}}'", + "type-edge": "Bordure", + "type-edges": "Bordures", + "list-of-edges": "{ count, plural, 1 {Une bordure} other {List of # bordures} }", + "edge-name-starts-with": "Bordures dont les noms commencent par '{{prefix}}'", "search": "Recherche d'entités", "select-entities": "Sélectionner des entités", "selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées", @@ -885,6 +1059,14 @@ "make-public": "Rendre la vue d'entité publique", "make-public-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues publiques et accessibles à d'autres", "make-public-entity-view-title": "Voulez-vous vraiment que la vue de l'entité '{{entityViewName}}' soit publique?", + "assign-entity-view-to-edge": "Attribuer a la bordure", + "assign-entity-view-to-edge-text":"Veuillez sélectionner la bordure auquel attribuer la ou les vues d'entité.", + "unassign-entity-view-from-edge-title": "Voulez-vous vraiment annuler l'attribution de la vue d'entité '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Après la confirmation, la vue de l'entité sera non attribuée et ne sera pas accessible par la bordure.", + "unassign-entity-views-from-edge-action-title": "Annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} } de la bordure", + "unassign-entity-view-from-edge": "Annuler l'attribution des vues d'entité", + "unassign-entity-views-from-edge-title": "Êtes-vous sûr de vouloir annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} }?", + "unassign-entity-views-from-edge-text": "Après la confirmation, toutes les vues des entités sélectionnées seront non attribuées et ne seront pas accessibles par la bordure.", "management": "Gestion de vue d'entité", "name": "Nom", "name-required": "Un nom est requis.", @@ -935,6 +1117,7 @@ "data-type": "Type de données", "entity": "Entité", "error": "erreur", + "type-edge-event": "Downlink", "errors-occurred": "Des erreurs sont survenues", "event": "événement", "event-time": "Heure de l'événement", @@ -1302,7 +1485,35 @@ "set-root": "Rend la chaîne de règles racine (root) ", "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", - "system": "Système" + "system": "Système", + "assign-rulechains": "Attribuer aux chaînes de règles", + "assign-new-rulechain": "Attribuer une nouvele chaînes de règles", + "delete-rulechains": "Supprimer une chaînes de règles", + "unassign-rulechain": "Retirer chaîne de règles", + "unassign-rulechains": "Retirer chaînes de règles", + "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", + "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}} de la bordure", + "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", + "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", + "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", + "set-edge-template-root-rulechain": "Rendre le modèle de bord de chaîne de règles racine", + "set-edge-template-root-rulechain-title": "Voulez-vous vraiment définir la racine du modèle d'arête de la chaîne de règles '{{ruleChainName}}'?", + "set-edge-template-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra la racine du modèle d'arête et sera la chaîne de règles racine pour les arêtes nouvellement créées.", + "invalid-rulechain-type-error": "Impossible d'importer la chaîne de règles: type de chaîne de règles non valide. Le type attendu est {{attenduRuleChainType}}.", + "set-auto-assign-to-edge": "Attribuer une chaîne de règles aux arêtes lors de la création", + "set-auto-assign-to-edge-title": "Voulez-vous vraiment attribuer automatiquement la chaîne de règles d'arête '{{ruleChainName}}' à l'arête (s) lors de la création?", + "set-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arête sera automatiquement affectée à l'arête (s) lors de la création.", + "unset-auto-assign-to-edge": "Non défini, attribuer une chaîne de règles aux arêtes lors de la création", + "unset-auto-assign-to-edge-title": "Voulez-vous vraiment annuler l'attribution de la chaîne de règles d'arête \"{{ruleChainName}}\" aux arêtes lors de la création?", + "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", + "edge-template-root": "Racine du modèle", + "search": "Rechercher des chaînes de règles", + "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", + "open-rulechain": "Chaîne de règles ouverte", + "assign-to-edge": "Attribuer à Edge", + "edge-rulechain": "Chaîne de règles Edge", + "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" }, "rulenode": { "add": "Ajouter un noeud de règle",