diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index b19c5d40b9..8a80aa80f1 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -455,6 +455,24 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } + }, + { + "alias": "update_json_attribute", + "name": "Update JSON attribute", + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADhCAMAAACZWwyuAAAB71BMVEX////w8PDt7e3z8/Pg4ODAwMDc3Nz19fUZhJQ4lKK9vb17e3v9/f2mpqbj4+P4+Pi+vr58fHzT09P7+/uTxcz2+/ucy9GpqakCAgK93OHNzc0eh5b09PSw1dusrKwvkZ+ampqysrIzMzPV1dXCwsKurq4iiZnZ2dnX19fIyMgrj50ABsiKiopmZmbo8/XQ5+ro6OjPz89JnqqCgoLX6u2cnJwmjJv39/ff39+12N7ExMQ2NjaioqKYmJjs7OzD4OTS0tLKysrh8PLq6uq6uroCCMjDw8OOjo6hzdO0tLQuj56UlJS4uvDZ7O7l5eVvsrw0k6EchZWEhIRwcHDU6ezG4eaGv8d1tb+3t7dCmqeIiIj4+/3j8fO62+AKD8tWpbGgoKBqamouLi7u7u47lqTP0PWr0tgXHM1/u8N6uMFqsLpfqrVNoKxFRUXU1faYyM92dnY7Ozvz+Pry8vK5ubm4uLh+fn5sbGzGx/OipOtzduG42t8uM9KNw8pTo690dHT6+vrt9viOkefb29um0NYfHx/p6vu9vvGqrO2YmulXW9uhztRnrrgmJiYQEBDt7fzk5frf4PjL4+eMj+a/3eJpbN9MUNk7P9SQxMuNwslfX19TU1OcnurI4uY9QdYyN9ImK9BVVVVPT097fuNYWFjSJwPyAAALvElEQVR42uzbTWviQBjA8ad5qgNONI05FCSIshUNKERCSFlM2WIxSKCH5FI8RfoFCrvVYwv7xTfG4susxqgpqJkfHoLM6c9MTCYROI7jOC4doiIRoqrEkhQRuIOJEsEVROI1Dw2pIkPlMQ8PyWMez8ItLOD2QnArAlxyIsEYhK/x5CSMJQGX0Cfu8AmXTCEqbkWUPQaBiDtd8hJXMJ6SbNByeWd3iROMRxIPAhEx09MSd1ATDwIJMdPTEndJNIid4D/kkLk4yMbFZTop2fXdonSZktb2XuFKpwRnBzeoGoeklNZS1tjj+BVu2hDKfYhxKXUXTtnXHIqgXd+esk/pNSK+GrQcjZsbuotBVrKUFmzSGs++b1djZqVV1m01D6crqlSpVOldpYKFSmzKWUCN0nI0bq5bWAwiuHquNNnj+JOlSDWA144OJW86KUcp38oAtQZIPZ/mICQEBvWGcLpw7i6acu0iCkXDtQ1hcos4MtdTOt4VYs2h5dk42y4azj3ejhYpVUyEwEZ1F+BpBJKviQVfWUkZuOTZz0e5ba8FJ4xNWZcLPw0De97vezpYTzmUK1iiL18p5T+aEaymxGRU2OhhTMAbgFISATvCMqU0LgEUo4U/KOreKV9LsSlHAeKtgY+09STr6ylvzABfRuWvlC5i4DCz8oiU4Az6Uwug25blzsMyZb8zDpkQkgj04YSxKSf16FwpTOquzZwrnx5lvT3sLlP21lKSoxY4aM4wANCn10DmKemvaFZ2HuBMMCnDT5QSe205x6R8ufICWqowKQ/92WFJ/lsTID+9vhp29FlKpye1/AbUXELqTTgDbMq60S2PDMRHKutsSqzTGrIpD74YYvUohBr+x1D+O0tZMqaO0wASfPjmWdwjbfgFd0wDUZjY+F/Ke6ptSRl/iZ6Nm3Dc4sq7Se/GMRtbQ7hZvjrR+XZGKimrhoZ7pOSbbKntV/Kt39R20fkDiZSf7Yi4i3rBkzLVJ46g4M7mHH+l4Bjpv+hiXfTyTr9lVk+U38Diqzs1n1l8XeibiJaKLP7O74FEiYdMT/RfE0RVJZICe8txSSRKmU9KEIRm8V0QnitNIWvST6m93whhy3/sm/lTEmEYx7/SOLPbdFgjCDGEWm0FBUVEGRRF0WEmlCgxeFSaeKHpNF1ajWW3V4cddh9/aLs078PqCMOCxjrs55dn3mff9/3hM7x7fGfYrSs1ll9leUWFrmrK+ERXahSk0uc9srTKJ7c31+hKjQJUHnnorc2g0lijK9eVGgWovHF1fUaV5zSVyg54RUaVNzWVearcK5FWWWPcqanMU2VjU1NTI6kcNjbpNJX5HvDGq43pA7510/Q+TaVylcyl/F65z1iuqVSi8vgpX23VqaUfO5c1lUpUrq+V2LXUh6PxnPbhuExxhnHrfi3OKFylbseV8ic/tz/WlRoroFK8V9ZMGad0pUZuKstyBSWMpnJ1qfS0tXmk6hhFHmRfR/sXnQJUVswPJy8dy0Vlvd9fL1WDFXmRfR3tv5hn39oT9+JU5TyyNwBQi8rOz3vWVnurc1HZ5XR2rZhK2n8x68buNbwNPqIq43lCVSrLpB/k1aZcVD5wODoAXOQk9ND3hD39DrzhzR39QgiIhs22Xj1YH4EQL5hjQIQ3C5OO9DrbWaCSZ9dpPu2PjdeuHZK7BPD+JdU0g4nrQVWplPizJxeVAaAVgLvF5na7gT6+rnlgAJZ6p6ujWfCD72uesEWpDxP3qvlrGzA+6hwNh2gdU8mus/m0PzYYjYexkO/3qBLxsbtQnUpf8vDCRi4HNcBFADcXsPDoDcF1FnwlYOFaWV9UpQdjiGfrSCW7TvPTVB/FQl4knlIlfr+H6lRWDZ8uU66yjjOLcM2Syj64DCmV41wd65PK1sl6m9WWUSXNz8zd9jtUiaftg6pTuXX4VFkeKk2c84KIXlTZE2IqRzgT65PKrgF/pEem0jpEKuX7ZDPZQDXNC3swGLSnnkNqUbnfW6PwFb1bgEgr1w0JUSXAVJq4AOszVSNcHTBho3VwRUkl7bOQg9WQMdj+nOo/vnwBMDMoEnwxA0AtKm/MbxE5qUBlhGt5Z9AjKtw3WSwylTFLd7gfrM9Uge/wO3ts6XWfhFf+PlJJ84nFj52XH66LxKli1m5/DQl2wM9MX1aFyl//8koFKvHRY3Y5sO5+2Gxtkau0CpMjYH1SdaFDMNf3p9fpo8LF+hBdp/nEopehH3aJ11QRHxuLL1D5y3hbFSqXAvnAV6JIbJuu1lQuC0eStRs1lctC1aU16njsaCFb8VRW5pCxvWnBauW/5ZXZDvKQJ0K6ednYMarGXHIFVFZ99ib36BTklRlVRjmTXCWNDdaccsmgXaIBSlGPSt9BMa/0KcgrM6rUOyFTSWNSmT2XxB2RucQsFKMalRLztxTklbxVEKJ6FktQXhngOK4OIiOTgjXGszHllA5p1Mo1L5FLErPtd6EcFansXDt8TkFeyff7J4RPTCXllXG3+2JKXX/YYhFVsjHllLFewCLEs+WSc2MoOoWoXFtbu6kzg8pM74+9LlJJeSWQUhfg/Kk+G9MBb+GB3g5kySWfioFF0SlE5b4TV4YvKVQ5ZCWVLK9k6tq4kaVVOjk3YkPIwtuECv4gV+C90uc9pkzlWRvljiyvZOpMmVSiq2WciyALH+ZQfApUuV65SsodKRkCPN2p/NGS6rMx5ZQYCk+Es+SSmAk+Q/EpQOX5mgpf8mGZYpUsd5SpfDBgMugR8hikPhtTTolxwdqDzLkkntlHUHzyV9l5I+lNHmhUrpLljjKVzpg55kC8x2a2hsDGlFMCvVwEmXNJPEpABayOOMPggvpZBSrHnRaPCepnBVRuOFFefmIDlg3TX/buXqVhKIzD+FuonEEJJQ4eKogEOrjFQRRSisEiuIl3YPYOvQId7Kg34OSdGgd1SaD5aPmH8zwQsv8g5Jx3OOf08c0GUP+UUezMXDzA8/3lKHNnZS630Oqf8sAK//5QvkKrG+VVsqygXEQvGyibUc5WlZTl44+gbEJ5u5pm1ZT3/hjKBpSLr5NRDeWFf4WyyR78eVRHeecnUG5PebOOaynt0K8ttNpTzpMsy5LkvIry+uNpZqHVnjK6LMvm4yrKqXd84A2X6PzBd0mZux/KERvHfsYZE884o5ch2/LscxOe5C4orfDFxMJrAFP0oQQllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBuWdK2qLwDj8lIiIiIiIKLpdG45ZFqTP6y407heV/aTfK1Oi3qBtlgNdmfLdrxypyw0AAhifJ2AeDZtcohUAEgZGFrdJVjIsgk2DY5urt9xFu9wn2xWMFLklzV3gb45u/GHD7YSML5s3wwUASSqHcbkIplNtLKIVyewll7onaZR7oB/yfJ/UYZfvLmbMXyscpy2CKxplOKB+mtBwRNRcflLJJvhh/Anybir4nBV9sMeJKyonnZaYO/cm4GX1+JC6xr02dPgAlpaFpFMTq7onUsRkx0WEd5RD42ucP3YWJeMJQI4YTJj7H2tz3TzkBlPR0oQEgkUK6AEzz2mPnxhx6LPsW0RA2XHYc8ebKwXPcP2UPgPTynAk9KV2pTHtcRZkxZ8Ma9dkZvqJmO3KLjnPFTimP1EEm/PyP8tMfSp8p7/R9FWWyy9BMrTnp1lwRA53zNx70UrtTSmjiMhIdXykvdADQpDCT2katoiTWiC1Xli0OmbJyZkQ8m4XR7/XYASQ7JNLwSglxfh4qUl+bETUNsIqyM66YAyfPdaz5hpiYuzzDWLHeKyUcYhEH9Zcy/wzZRApebDUOa2873dWZ2iJG54oQENHVuGRrE+Ju30q5g28qoRTK7SWUQrm9hFIot5dQCuX2kvUrWQrcXrKqKgvUkiRJkiRJkiS9328dj4CaN1dagAAAAABJRU5ErkJggg==", + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}", + "dataKeySettingsSchema": "{}", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + } } ] } diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index 81d3390dd2..500d1bd06e 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller public class WebConfig { - @RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") + @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"}) public String redirect() { return "forward:/index.html"; } 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 3f32e878f4..9453bd46b7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -46,8 +46,6 @@ 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.UUID; - @RestController @TbCoreComponent @RequestMapping("/api") @@ -177,7 +175,6 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false) String offset, @RequestParam(required = false) Boolean fetchOriginator ) throws ThingsboardException { checkParameter("EntityId", strEntityId); @@ -191,12 +188,9 @@ public class AlarmController extends BaseController { } checkEntityId(entityId, Operation.READ); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - UUID idOffsetUuid = null; - if (StringUtils.isNotEmpty(offset)) { - idOffsetUuid = toUUID(offset); - } + try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator, idOffsetUuid)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 8d46cce8e1..bc9b91cec4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -110,7 +110,6 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -130,6 +129,7 @@ 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; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index ac9e5a3ca2..7e49ba55d4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -18,12 +18,14 @@ package org.thingsboard.server.controller; 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.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.RestController; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -31,6 +33,7 @@ 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.event.EventService; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -101,4 +104,38 @@ public class EventController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) + @ResponseBody + public PageData getEvents( + @PathVariable("entityType") String strEntityType, + @PathVariable("entityId") String strEntityId, + @RequestParam("tenantId") String strTenantId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestBody EventFilter eventFilter, + @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("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + checkEntityId(entityId, Operation.READ); + + if(sortProperty != null && sortProperty.equals("createdTime") && eventFilter.hasFilterForJsonBody()) { + sortProperty = ModelConstants.CREATED_TIME_PROPERTY; + } + + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(eventService.findEventsByFilter(tenantId, entityId, eventFilter, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 85ec0950e3..d5fcc2706f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -244,7 +244,8 @@ public class RuleChainController extends BaseController { } RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE); - RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); + checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData) ? true : null); + RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId())); if (RuleChainType.CORE.equals(ruleChain.getType())) { tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); 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 3ac679e0d7..5e924a6d29 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -36,8 +36,8 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mObject; 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.resource.TbResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -52,12 +52,6 @@ public class TbResourceController extends BaseController { public static final String RESOURCE_ID = "resourceId"; - private final TbResourceService resourceService; - - public TbResourceController(TbResourceService resourceService) { - this.resourceService = resourceService; - } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET) @ResponseBody 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 index 316707b9ef..c3f5346170 100644 --- 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 @@ -40,8 +40,6 @@ public class DeviceProfileMsgConstructor { .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) { @@ -54,6 +52,12 @@ public class DeviceProfileMsgConstructor { if (deviceProfile.getDescription() != null) { builder.setDescription(deviceProfile.getDescription()); } + if (deviceProfile.getTransportType() != null) { + builder.setTransportType(deviceProfile.getTransportType().name()); + } + if (deviceProfile.getProvisionType() != null) { + builder.setProvisionType(deviceProfile.getProvisionType().name()); + } if (deviceProfile.getProvisionDeviceKey() != null) { builder.setProvisionDeviceKey(deviceProfile.getProvisionDeviceKey()); } 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 index 9b204d3187..1546572531 100644 --- 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 @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.edge.rpc.constructor; +import com.google.gson.reflect.TypeToken; 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; @@ -82,7 +82,7 @@ public class EntityDataMsgConstructor { 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); + List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType()); attributeDeleteMsg.addAllAttributeNames(keys); attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); 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 3d21c66e23..ec0daf2d71 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 @@ -58,11 +58,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; 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; 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 158cadc1a3..67d981e2fa 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 @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -97,7 +97,7 @@ public class InstallScripts { private OAuth2ConfigTemplateService oAuth2TemplateService; @Autowired - private TbResourceService resourceService; + private ResourceService resourceService; private Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); @@ -259,18 +259,12 @@ 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/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index ca70c4206e..296d9f0a8d 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 @@ -159,7 +159,7 @@ public class CassandraDbHelper { } 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(); + str = Boolean.valueOf(row.getBoolean(index)).toString(); } else { str = row.getString(index); } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java new file mode 100644 index 0000000000..81c59609ff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -0,0 +1,202 @@ +/** + * 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.resource; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +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.LwM2mResourceObserve; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; +import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Slf4j +@Service +public class DefaultTbResourceService implements TbResourceService { + + private final ResourceService resourceService; + private final DDFFileParser ddfFileParser; + + public DefaultTbResourceService(ResourceService resourceService) { + this.resourceService = resourceService; + this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + } + + @Override + public TbResource saveResource(TbResource resource) throws ThingsboardException { + log.trace("Executing saveResource [{}]", resource); + if (StringUtils.isEmpty(resource.getData())) { + throw new DataValidationException("Resource data should be specified!"); + } + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + try { + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (!objectModels.isEmpty()) { + ObjectModel objectModel = objectModels.get(0); + + String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); + String name = objectModel.name; + resource.setResourceKey(resourceKey); + 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())); + } + } catch (InvalidDDFFileException | IOException e) { + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } + if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource) == null) { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } else { + resource.setResourceKey(resource.getFileName()); + } + + return resourceService.saveResource(resource); + } + + @Override + public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + return resourceService.getResource(tenantId, resourceType, resourceId); + } + + @Override + public TbResource findResourceById(TenantId tenantId, TbResourceId resourceId) { + return resourceService.findResourceById(tenantId, resourceId); + } + + @Override + public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { + return resourceService.findResourceInfoById(tenantId, resourceId); + } + + @Override + public PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + } + + @Override + public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + return resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + } + + @Override + public List findLwM2mObject(TenantId tenantId, String sortOrder, String sortProperty, String[] objectIds) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + List resources = resourceService.findTenantResourcesByResourceTypeAndObjectIds(tenantId, ResourceType.LWM2M_MODEL, + objectIds); + return resources.stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + PageData resourcePageData = resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, ResourceType.LWM2M_MODEL, pageLink); + return resourcePageData.getData().stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public void deleteResource(TenantId tenantId, TbResourceId resourceId) { + resourceService.deleteResource(tenantId, resourceId); + } + + @Override + public void deleteResourcesByTenantId(TenantId tenantId) { + resourceService.deleteResourcesByTenantId(tenantId); + } + + private Comparator getComparator(String sortProperty, String sortOrder) { + Comparator comparator; + if ("name".equals(sortProperty)) { + comparator = Comparator.comparing(LwM2mObject::getName); + } else { + comparator = Comparator.comparingLong(LwM2mObject::getId); + } + return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; + } + + private LwM2mObject toLwM2mObject(TbResource resource) { + try { + DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (objectModels.size() == 0) { + return null; + } else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(resource.getResourceKey()); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); + } + }); + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); + return null; + } + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java similarity index 90% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java rename to application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 0093b43dfe..cc35eac26e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -13,24 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.resource; +package org.thingsboard.server.service.resource; -import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; -import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import java.io.IOException; import java.util.List; - public interface TbResourceService { - TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException; + + TbResource saveResource(TbResource resource) throws ThingsboardException; TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); @@ -55,4 +54,5 @@ public interface TbResourceService { void deleteResource(TenantId tenantId, TbResourceId resourceId); void deleteResourcesByTenantId(TenantId tenantId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index b3516bd364..4f38cf7a23 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -17,8 +17,10 @@ package org.thingsboard.server.service.security.auth.oauth2; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +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.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -85,6 +87,10 @@ public abstract class AbstractOAuth2ClientMapper { @Autowired protected TbClusterService tbClusterService; + @Value("${edges.enabled}") + @Getter + private boolean edgesEnabled; + private final Lock userCreationLock = new ReentrantLock(); protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) { @@ -171,6 +177,9 @@ public abstract class AbstractOAuth2ClientMapper { tenant.setTitle(tenantName); tenant = tenantService.saveTenant(tenant); installScripts.createDefaultRuleChains(tenant.getId()); + if (edgesEnabled) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } tenantProfileCache.evict(tenant.getId()); tbClusterService.onTenantChange(tenant, null); tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(), diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index af91827fc6..80387bb686 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -57,7 +57,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; @@ -117,7 +117,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; private final DeviceProvisionService deviceProvisionService; - private final TbResourceService resourceService; + private final ResourceService resourceService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -126,7 +126,7 @@ public class DefaultTransportApiService implements TransportApiService { RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, - DeviceProvisionService deviceProvisionService, TbResourceService resourceService) { + DeviceProvisionService deviceProvisionService, ResourceService resourceService) { this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.apiUsageStateService = apiUsageStateService; diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt index 111465dd8a..57f907c5e0 100644 --- a/application/src/main/resources/banner.txt +++ b/application/src/main/resources/banner.txt @@ -1,8 +1,8 @@ - ______ __ _ __ __ - /_ __/ / /_ (_) ____ ____ _ _____ / /_ ____ ____ _ _____ ____/ / - / / / __ \ / / / __ \ / __ `/ / ___/ / __ \ / __ \ / __ `/ / ___/ / __ / - / / / / / / / / / / / / / /_/ / (__ ) / /_/ // /_/ // /_/ / / / / /_/ / -/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_.___/ \____/ \__,_/ /_/ \__,_/ + ______ __ _ ____ __ + /_ __/ / /_ (_) ____ ____ _ _____ / __ ) ____ ____ _ _____ ____/ / + / / / __ \ / / / __ \ / __ `/ / ___/ / __ | / __ \ / __ `/ / ___/ / __ / + / / / / / / / / / / / / / /_/ / (__ ) / /_/ / / /_/ // /_/ / / / / /_/ / +/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_____/ \____/ \__,_/ /_/ \__,_/ /____/ =================================================== diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 61caeb61fb..b2f19748b6 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -38,6 +38,9 @@ + + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 519d0b5c94..1acc48077e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -243,6 +243,7 @@ sql: batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + update_by_latest_ts: "${SQL_TS_UPDATE_BY_LATEST_TIMESTAMP:true}" # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:false}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert @@ -516,7 +517,7 @@ js: # Built-in JVM JavaScript environment properties local: # Use Sandboxed (secured) JVM JavaScript environment - use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:false}" + use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}" # Specify thread pool size for JavaScript sandbox resource monitor monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index ba2d3ce61e..246749881f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -38,6 +38,7 @@ 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.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; @@ -672,28 +673,32 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.expectMessageAmount(7); edgeImitator.connect(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertEquals(7, edgeImitator.getDownlinkMsgs().size()); Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceProfileUpdateMsg.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); + edgeImitator.expectMessageAmount(7); doPost("/api/edge/sync/" + edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); - Assert.assertEquals(4, edgeImitator.getDownlinkMsgs().size()); + Assert.assertEquals(7, edgeImitator.getDownlinkMsgs().size()); Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceProfileUpdateMsg.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(); + try { + edgeImitator.disconnect(); + } catch (Exception ignored) {} doDelete("/api/device/" + savedDevice.getId().getId().toString()) .andExpect(status().isOk()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java index 5edde458ce..c07a499f83 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java @@ -42,7 +42,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class BaseEdgeEventControllerTest extends AbstractControllerTest { private Tenant savedTenant; - private TenantId tenantId; private User tenantAdmin; @Before @@ -52,7 +51,6 @@ public class BaseEdgeEventControllerTest extends AbstractControllerTest { Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); savedTenant = doPost("/api/tenant", tenant, Tenant.class); - tenantId = savedTenant.getId(); Assert.assertNotNull(savedTenant); tenantAdmin = new User(); @@ -63,6 +61,10 @@ public class BaseEdgeEventControllerTest extends AbstractControllerTest { tenantAdmin.setLastName("Downs"); tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + // sleep 1 seconds to avoid CREDENTIALS updated message for the user + // user credentials is going to be stored and updated event pushed to edge notification service + // while service will be processing this event edge could be already added and additional message will be pushed + Thread.sleep(1000); } @After @@ -75,7 +77,6 @@ public class BaseEdgeEventControllerTest extends AbstractControllerTest { @Test public void testGetEdgeEvents() throws Exception { - Thread.sleep(500); Edge edge = constructEdge("TestEdge", "default"); edge = doPost("/api/edge", edge, Edge.class); @@ -83,29 +84,31 @@ public class BaseEdgeEventControllerTest extends AbstractControllerTest { 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()); + // wait while edge event for the relation entity persisted to DB + Thread.sleep(100); + List edgeEvents; + int attempt = 1; + do { + edgeEvents = doGetTypedWithTimePageLink("/api/edge/" + edge.getId().toString() + "/events?", + new TypeReference>() {}, new TimePageLink(4)).getData(); + attempt++; + Thread.sleep(100); + } while (edgeEvents.size() != 4 && attempt < 5); 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()); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RULE_CHAIN.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.DEVICE.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.ASSET.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RELATION.equals(ee.getType()))); } private Device constructDevice(String name, String type) { diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java index 6622a744b5..92548bb621 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -32,10 +32,12 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; 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.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; @@ -45,6 +47,15 @@ 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.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -57,6 +68,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -71,7 +85,6 @@ 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; @@ -81,6 +94,7 @@ 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.EdgeConfiguration; @@ -106,6 +120,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; +import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -114,6 +129,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j abstract public class BaseEdgeTest extends AbstractControllerTest { + private static final String CUSTOM_DEVICE_PROFILE_NAME = "Thermostat"; + private Tenant savedTenant; private TenantId tenantId; private User tenantAdmin; @@ -145,17 +162,25 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { tenantAdmin.setLastName("Downs"); tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + // sleep 1 seconds to avoid CREDENTIALS updated message for the user + // user credentials is going to be stored and updated event pushed to edge notification service + // while service will be processing this event edge could be already added and additional message will be pushed + Thread.sleep(1000); + 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); + // TODO: voba - should be less, but events from SyncEdgeService stack with events from controller. will be fixed in next releases + // so ideally sync process should check current edge queue and add only missing entities to the edge queue + edgeImitator.expectMessageAmount(10); edgeImitator.connect(); } @After public void afterTest() throws Exception { - edgeImitator.disconnect(); + try { + edgeImitator.disconnect(); + } catch (Exception ignored) {} loginSysAdmin(); @@ -163,23 +188,63 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { .andExpect(status().isOk()); } - @Test public void test() throws Exception { testReceivedInitialData(); + int expectedDownlinkSize = 10; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testDevices(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testAssets(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testRuleChains(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testDashboards(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testRelations(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testAlarms(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testEntityView(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testCustomer(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testWidgetsBundleAndWidgetType(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testTimeseries(); + expectedDownlinkSize = expectedDownlinkSize + 1; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testAttributes(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testSendMessagesToCloud(); + expectedDownlinkSize = expectedDownlinkSize + 9; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + testRpcCall(); + expectedDownlinkSize = expectedDownlinkSize + 1; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); } private Device findDeviceByName(String deviceName) throws Exception { @@ -204,10 +269,10 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { return asset; } - private Device saveDevice(String deviceName) throws Exception { + private Device saveDevice(String deviceName, String type) throws Exception { Device device = new Device(); device.setName(deviceName); - device.setType("test"); + device.setType(type); return doPost("/api/device", device, Device.class); } @@ -233,7 +298,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceRpcCallMsg); @@ -243,7 +308,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testReceivedInitialData() throws Exception { log.info("Checking received data"); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); EdgeConfiguration configuration = edgeImitator.getConfiguration(); Assert.assertNotNull(configuration); @@ -253,9 +318,9 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { UserId userId = edgeImitator.getUserId(); Assert.assertNotNull(userId); - Optional optionalMsg1 = edgeImitator.findMessageByType(DeviceUpdateMsg.class); - Assert.assertTrue(optionalMsg1.isPresent()); - DeviceUpdateMsg deviceUpdateMsg = optionalMsg1.get(); + Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.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); @@ -264,9 +329,25 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { 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(); + List deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class); + Assert.assertEquals(3, deviceProfileUpdateMsgList.size()); + Optional deviceProfileUpdateMsgOpt = + deviceProfileUpdateMsgList.stream().filter(dfum -> CUSTOM_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny(); + Assert.assertTrue(deviceProfileUpdateMsgOpt.isPresent()); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceProfileUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + UUID deviceProfileUUID = new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB()); + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileUUID.toString(), DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + Assert.assertNotNull(deviceProfile.getProfileData()); + Assert.assertNotNull(deviceProfile.getProfileData().getAlarms()); + Assert.assertNotNull(deviceProfile.getProfileData().getAlarms().get(0).getClearRule()); + + testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsg); + + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.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); @@ -277,9 +358,9 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { testAutoGeneratedCodeByProtobuf(assetUpdateMsg); - Optional optionalMsg3 = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); - Assert.assertTrue(optionalMsg3.isPresent()); - RuleChainUpdateMsg ruleChainUpdateMsg = optionalMsg3.get(); + Optional ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); + Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); + RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.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); @@ -296,13 +377,12 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testDevices() throws Exception { log.info("Testing devices"); - Device savedDevice = saveDevice("Edge Device 2"); - + // 1 edgeImitator.expectMessageAmount(1); + Device savedDevice = saveDevice("Edge Device 2", "Default"); doPost("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; @@ -312,11 +392,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(deviceUpdateMsg.getName(), savedDevice.getName()); Assert.assertEquals(deviceUpdateMsg.getType(), savedDevice.getType()); + // 2 edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; @@ -324,11 +404,34 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + // 3 edgeImitator.expectMessageAmount(1); doDelete("/api/device/" + savedDevice.getId().getId().toString()) .andExpect(status().isOk()); - edgeImitator.waitForMessages(); + // we should not get any message because device is not assigned to edge any more + Assert.assertFalse(edgeImitator.waitForMessages(1)); + // 4 + edgeImitator.expectMessageAmount(1); + savedDevice = saveDevice("Edge Device 3", "Default"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof 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()); + + // 5 + edgeImitator.expectMessageAmount(1); + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + // in this case we should get messages because device was assigned to edge + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; @@ -342,13 +445,13 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testAssets() throws Exception { log.info("Testing assets"); - Asset savedAsset = saveAsset("Edge Asset 2"); + // 1 edgeImitator.expectMessageAmount(1); + Asset savedAsset = saveAsset("Edge Asset 2"); doPost("/api/edge/" + edge.getId().getId().toString() + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) latestMessage; @@ -358,11 +461,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(assetUpdateMsg.getName(), savedAsset.getName()); Assert.assertEquals(assetUpdateMsg.getType(), savedAsset.getType()); + // 2 edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getId().getId().toString() + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); assetUpdateMsg = (AssetUpdateMsg) latestMessage; @@ -370,11 +473,32 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + // 3 edgeImitator.expectMessageAmount(1); doDelete("/api/asset/" + savedAsset.getId().getId().toString()) .andExpect(status().isOk()); - edgeImitator.waitForMessages(); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + // 4 + edgeImitator.expectMessageAmount(1); + savedAsset = saveAsset("Edge Asset 3"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof 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()); + + // 5 + edgeImitator.expectMessageAmount(1); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); assetUpdateMsg = (AssetUpdateMsg) latestMessage; @@ -387,21 +511,21 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testRuleChains() throws Exception { log.info("Testing RuleChains"); + + // 1 + edgeImitator.expectMessageAmount(1); 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 + // sleep 1 seconds to avoid ENTITY_UPDATED_RPC_MESSAGE for the rule chain + // rule chain metadata is going to be stored and updated event pushed to edge notification service + // while service will be processing this event assignment rule chain to edge will be completed if bad timing Thread.sleep(1000); - - edgeImitator.expectMessageAmount(1); doPost("/api/edge/" + edge.getId().getId().toString() + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; @@ -410,13 +534,14 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); + // 2 testRuleChainMetadataRequestMsg(savedRuleChain.getId()); + // 3 edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getId().getId().toString() + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; @@ -424,17 +549,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + // 4 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()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); log.info("RuleChains tested successfully"); } @@ -452,8 +571,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); @@ -502,15 +621,15 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testDashboards() throws Exception { log.info("Testing Dashboards"); + + // 1 + edgeImitator.expectMessageAmount(1); 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; @@ -518,25 +637,24 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); Assert.assertEquals(dashboardUpdateMsg.getTitle(), savedDashboard.getName()); - testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); + // 2 edgeImitator.expectMessageAmount(1); savedDashboard.setTitle("Updated Edge Test Dashboard"); doPost("/api/dashboard", savedDashboard, Dashboard.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(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()); + // 3 edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; @@ -544,17 +662,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + // 4 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()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); log.info("Dashboards tested successfully"); } @@ -562,19 +674,17 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testRelations() throws Exception { log.info("Testing Relations"); + // 1 + edgeImitator.expectMessageAmount(1); 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; @@ -588,6 +698,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); + // 2 edgeImitator.expectMessageAmount(1); doDelete("/api/relation?" + "fromId=" + relation.getFrom().getId().toString() + @@ -597,8 +708,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { "&toId=" + relation.getTo().getId().toString() + "&toType=" + relation.getTo().getEntityType().name()) .andExpect(status().isOk()); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); relationUpdateMsg = (RelationUpdateMsg) latestMessage; @@ -617,18 +727,17 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testAlarms() throws Exception { log.info("Testing Alarms"); - Device device = findDeviceByName("Edge Device 1"); + // 1 + edgeImitator.expectMessageAmount(1); + 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; @@ -639,10 +748,10 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(alarmUpdateMsg.getStatus(), savedAlarm.getStatus().name()); Assert.assertEquals(alarmUpdateMsg.getSeverity(), savedAlarm.getSeverity().name()); + // 2 edgeImitator.expectMessageAmount(1); doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/ack"); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; @@ -652,10 +761,10 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.ACTIVE_ACK.name()); + // 3 edgeImitator.expectMessageAmount(1); doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/clear"); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; @@ -665,26 +774,29 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.CLEARED_ACK.name()); + // 4 + edgeImitator.expectMessageAmount(1); doDelete("/api/alarm/" + savedAlarm.getId().getId().toString()) .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + log.info("Alarms tested successfully"); } private void testEntityView() throws Exception { log.info("Testing EntityView"); - Device device = findDeviceByName("Edge Device 1"); + // 1 + edgeImitator.expectMessageAmount(1); + 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; @@ -697,11 +809,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(entityViewUpdateMsg.getEntityIdLSB(), device.getUuidId().getLeastSignificantBits()); Assert.assertEquals(entityViewUpdateMsg.getEntityType().name(), device.getId().getEntityType().name()); + // 2 edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getId().getId().toString() + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; @@ -709,17 +821,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + // 3 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()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); log.info("EntityView tested successfully"); } @@ -727,15 +833,14 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testCustomer() throws Exception { log.info("Testing Customer"); + // 1 + edgeImitator.expectMessageAmount(1); 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) latestMessage; @@ -743,13 +848,12 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); Assert.assertEquals(customerUpdateMsg.getTitle(), savedCustomer.getTitle()); - testAutoGeneratedCodeByProtobuf(customerUpdateMsg); + // 2 edgeImitator.expectMessageAmount(1); doDelete("/api/customer/edge/" + edge.getId().getId().toString(), Edge.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); customerUpdateMsg = (CustomerUpdateMsg) latestMessage; @@ -757,17 +861,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + // 3 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()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); log.info("Customer tested successfully"); } @@ -775,13 +873,12 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testWidgetsBundleAndWidgetType() throws Exception { log.info("Testing WidgetsBundle and WidgetType"); + // 1 + edgeImitator.expectMessageAmount(1); WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("Test Widget Bundle"); - - edgeImitator.expectMessageAmount(1); WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; @@ -790,20 +887,18 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(widgetsBundleUpdateMsg.getIdLSB(), savedWidgetsBundle.getUuidId().getLeastSignificantBits()); Assert.assertEquals(widgetsBundleUpdateMsg.getAlias(), savedWidgetsBundle.getAlias()); Assert.assertEquals(widgetsBundleUpdateMsg.getTitle(), savedWidgetsBundle.getTitle()); - testAutoGeneratedCodeByProtobuf(widgetsBundleUpdateMsg); + // 2 + edgeImitator.expectMessageAmount(1); 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(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); WidgetTypeUpdateMsg widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; @@ -814,11 +909,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(widgetTypeUpdateMsg.getName(), savedWidgetType.getName()); Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); + // 3 edgeImitator.expectMessageAmount(1); doDelete("/api/widgetType/" + savedWidgetType.getId().getId().toString()) .andExpect(status().isOk()); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; @@ -826,11 +921,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(widgetTypeUpdateMsg.getIdMSB(), savedWidgetType.getUuidId().getMostSignificantBits()); Assert.assertEquals(widgetTypeUpdateMsg.getIdLSB(), savedWidgetType.getUuidId().getLeastSignificantBits()); + // 4 edgeImitator.expectMessageAmount(1); doDelete("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString()) .andExpect(status().isOk()); - edgeImitator.waitForMessages(); - + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; @@ -843,15 +938,15 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void testTimeseries() throws Exception { log.info("Testing timeseries"); - Device device = findDeviceByName("Edge Device 1"); + edgeImitator.expectMessageAmount(1); + 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); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(edgeEvent); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityDataProto); @@ -890,7 +985,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityDataProto); @@ -916,7 +1011,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityDataProto); @@ -941,7 +1036,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); edgeEventService.saveAsync(edgeEvent1); clusterService.onEdgeEventUpdate(tenantId, edge.getId()); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityDataProto); @@ -996,8 +1091,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); @@ -1013,7 +1108,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void sendDeviceWithNameThatAlreadyExistsOnCloud() throws Exception { String deviceOnCloudName = RandomStringUtils.randomAlphanumeric(15); - Device deviceOnCloud = saveDevice(deviceOnCloudName); + Device deviceOnCloud = saveDevice(deviceOnCloudName, "Default"); UUID uuid = Uuids.timeBased(); @@ -1033,8 +1128,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); @@ -1063,7 +1158,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(1); doPost("/api/relation", relation); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForMessages()); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); RelationRequestMsg.Builder relationRequestMsgBuilder = RelationRequestMsg.newBuilder(); @@ -1078,8 +1173,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); @@ -1115,7 +1210,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(edgeImitator.waitForResponses()); List alarms = doGetTypedWithPageLink("/api/alarm/{entityType}/{entityId}?", @@ -1158,7 +1253,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(edgeImitator.waitForResponses()); EntityRelation relation = doGet("/api/relation?" + "&fromId=" + device2.getId().getId().toString() + @@ -1212,7 +1307,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder2); edgeImitator.sendUplinkMsg(uplinkMsgBuilder2.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(edgeImitator.waitForResponses()); // Wait before device attributes saved to database before requesting them from controller Thread.sleep(1000); @@ -1243,8 +1338,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages());; AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); @@ -1270,8 +1365,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); @@ -1299,8 +1394,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); @@ -1327,7 +1422,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(edgeImitator.waitForResponses()); } private void sendDeviceRpcResponse() throws Exception { @@ -1352,7 +1447,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(edgeImitator.waitForResponses()); } private void sendAttributesRequest() throws Exception { @@ -1383,8 +1478,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - edgeImitator.waitForResponses(); - edgeImitator.waitForMessages(); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityDataProto); @@ -1416,7 +1511,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { edgeImitator.expectResponsesAmount(1); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); - edgeImitator.waitForResponses(); + Assert.assertTrue(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?", @@ -1428,7 +1523,11 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void installation() throws Exception { edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); - Device savedDevice = saveDevice("Edge Device 1"); + DeviceProfile deviceProfile = this.createDeviceProfile(CUSTOM_DEVICE_PROFILE_NAME, null); + extendDeviceProfileData(deviceProfile); + doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + Device savedDevice = saveDevice("Edge Device 1", CUSTOM_DEVICE_PROFILE_NAME); doPost("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); @@ -1437,6 +1536,35 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); } + private void extendDeviceProfileData(DeviceProfile deviceProfile) { + DeviceProfileData profileData = deviceProfile.getProfileData(); + List alarms = new ArrayList<>(); + DeviceProfileAlarm deviceProfileAlarm = new DeviceProfileAlarm(); + deviceProfileAlarm.setAlarmType("High Temperature"); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setAlarmDetails("Alarm Details"); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setSpec(new SimpleAlarmConditionSpec()); + List condition = new ArrayList<>(); + AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); + alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + predicate.setValue(new FilterPredicateValue<>(55.0)); + alarmConditionFilter.setPredicate(predicate); + alarmConditionFilter.setValueType(EntityKeyValueType.NUMERIC); + condition.add(alarmConditionFilter); + alarmCondition.setCondition(condition); + alarmRule.setCondition(alarmCondition); + deviceProfileAlarm.setClearRule(alarmRule); + TreeMap createRules = new TreeMap<>(); + createRules.put(AlarmSeverity.CRITICAL, alarmRule); + deviceProfileAlarm.setCreateRules(createRules); + alarms.add(deviceProfileAlarm); + profileData.setAlarms(alarms); + profileData.setProvisionConfiguration(new AllowCreateNewDevicesDeviceProfileProvisionConfiguration("123")); + } + private EdgeEvent constructEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventActionType edgeEventAction, UUID entityId, EdgeEventType edgeEventType, JsonNode entityBody) { EdgeEvent edgeEvent = new EdgeEvent(); edgeEvent.setEdgeId(edgeId); 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 index 48ff61a491..6b4234f151 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -32,6 +32,7 @@ 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; @@ -58,6 +59,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; @Slf4j public class EdgeImitator { @@ -154,88 +156,93 @@ public class EdgeImitator { private ListenableFuture> processDownlinkMsg(DownlinkMsg downlinkMsg) { List> result = new ArrayList<>(); - if (downlinkMsg.getDeviceUpdateMsgList() != null && !downlinkMsg.getDeviceUpdateMsgList().isEmpty()) { + if (downlinkMsg.getDeviceUpdateMsgCount() > 0) { for (DeviceUpdateMsg deviceUpdateMsg: downlinkMsg.getDeviceUpdateMsgList()) { result.add(saveDownlinkMsg(deviceUpdateMsg)); } } - if (downlinkMsg.getDeviceCredentialsUpdateMsgList() != null && !downlinkMsg.getDeviceCredentialsUpdateMsgList().isEmpty()) { + if (downlinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : downlinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceProfileUpdateMsg)); + } + } + if (downlinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg: downlinkMsg.getDeviceCredentialsUpdateMsgList()) { result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg)); } } - if (downlinkMsg.getAssetUpdateMsgList() != null && !downlinkMsg.getAssetUpdateMsgList().isEmpty()) { + if (downlinkMsg.getAssetUpdateMsgCount() > 0) { for (AssetUpdateMsg assetUpdateMsg: downlinkMsg.getAssetUpdateMsgList()) { result.add(saveDownlinkMsg(assetUpdateMsg)); } } - if (downlinkMsg.getRuleChainUpdateMsgList() != null && !downlinkMsg.getRuleChainUpdateMsgList().isEmpty()) { + if (downlinkMsg.getRuleChainUpdateMsgCount() > 0) { for (RuleChainUpdateMsg ruleChainUpdateMsg: downlinkMsg.getRuleChainUpdateMsgList()) { result.add(saveDownlinkMsg(ruleChainUpdateMsg)); } } - if (downlinkMsg.getRuleChainMetadataUpdateMsgList() != null && !downlinkMsg.getRuleChainMetadataUpdateMsgList().isEmpty()) { + if (downlinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg: downlinkMsg.getRuleChainMetadataUpdateMsgList()) { result.add(saveDownlinkMsg(ruleChainMetadataUpdateMsg)); } } - if (downlinkMsg.getDashboardUpdateMsgList() != null && !downlinkMsg.getDashboardUpdateMsgList().isEmpty()) { + if (downlinkMsg.getDashboardUpdateMsgCount() > 0) { for (DashboardUpdateMsg dashboardUpdateMsg: downlinkMsg.getDashboardUpdateMsgList()) { result.add(saveDownlinkMsg(dashboardUpdateMsg)); } } - if (downlinkMsg.getRelationUpdateMsgList() != null && !downlinkMsg.getRelationUpdateMsgList().isEmpty()) { + if (downlinkMsg.getRelationUpdateMsgCount() > 0) { for (RelationUpdateMsg relationUpdateMsg: downlinkMsg.getRelationUpdateMsgList()) { result.add(saveDownlinkMsg(relationUpdateMsg)); } } - if (downlinkMsg.getAlarmUpdateMsgList() != null && !downlinkMsg.getAlarmUpdateMsgList().isEmpty()) { + if (downlinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg: downlinkMsg.getAlarmUpdateMsgList()) { result.add(saveDownlinkMsg(alarmUpdateMsg)); } } - if (downlinkMsg.getEntityDataList() != null && !downlinkMsg.getEntityDataList().isEmpty()) { + if (downlinkMsg.getEntityDataCount() > 0) { for (EntityDataProto entityData: downlinkMsg.getEntityDataList()) { result.add(saveDownlinkMsg(entityData)); } } - if (downlinkMsg.getEntityViewUpdateMsgList() != null && !downlinkMsg.getEntityViewUpdateMsgList().isEmpty()) { + if (downlinkMsg.getEntityViewUpdateMsgCount() > 0) { for (EntityViewUpdateMsg entityViewUpdateMsg: downlinkMsg.getEntityViewUpdateMsgList()) { result.add(saveDownlinkMsg(entityViewUpdateMsg)); } } - if (downlinkMsg.getCustomerUpdateMsgList() != null && !downlinkMsg.getCustomerUpdateMsgList().isEmpty()) { + if (downlinkMsg.getCustomerUpdateMsgCount() > 0) { for (CustomerUpdateMsg customerUpdateMsg: downlinkMsg.getCustomerUpdateMsgList()) { result.add(saveDownlinkMsg(customerUpdateMsg)); } } - if (downlinkMsg.getWidgetsBundleUpdateMsgList() != null && !downlinkMsg.getWidgetsBundleUpdateMsgList().isEmpty()) { + if (downlinkMsg.getWidgetsBundleUpdateMsgCount() > 0) { for (WidgetsBundleUpdateMsg widgetsBundleUpdateMsg: downlinkMsg.getWidgetsBundleUpdateMsgList()) { result.add(saveDownlinkMsg(widgetsBundleUpdateMsg)); } } - if (downlinkMsg.getWidgetTypeUpdateMsgList() != null && !downlinkMsg.getWidgetTypeUpdateMsgList().isEmpty()) { + if (downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { for (WidgetTypeUpdateMsg widgetTypeUpdateMsg: downlinkMsg.getWidgetTypeUpdateMsgList()) { result.add(saveDownlinkMsg(widgetTypeUpdateMsg)); } } - if (downlinkMsg.getUserUpdateMsgList() != null && !downlinkMsg.getUserUpdateMsgList().isEmpty()) { + if (downlinkMsg.getUserUpdateMsgCount() > 0) { for (UserUpdateMsg userUpdateMsg: downlinkMsg.getUserUpdateMsgList()) { onUserUpdate(userUpdateMsg); result.add(saveDownlinkMsg(userUpdateMsg)); } } - if (downlinkMsg.getUserCredentialsUpdateMsgList() != null && !downlinkMsg.getUserCredentialsUpdateMsgList().isEmpty()) { + if (downlinkMsg.getUserCredentialsUpdateMsgCount() > 0) { for (UserCredentialsUpdateMsg userCredentialsUpdateMsg: downlinkMsg.getUserCredentialsUpdateMsgList()) { result.add(saveDownlinkMsg(userCredentialsUpdateMsg)); } } - if (downlinkMsg.getDeviceRpcCallMsgList() != null && !downlinkMsg.getDeviceRpcCallMsgList().isEmpty()) { + if (downlinkMsg.getDeviceRpcCallMsgCount() > 0) { for (DeviceRpcCallMsg deviceRpcCallMsg: downlinkMsg.getDeviceRpcCallMsgList()) { result.add(saveDownlinkMsg(deviceRpcCallMsg)); } } - if (downlinkMsg.getDeviceCredentialsRequestMsgList() != null && !downlinkMsg.getDeviceCredentialsRequestMsgList().isEmpty()) { + if (downlinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg: downlinkMsg.getDeviceCredentialsRequestMsgList()) { result.add(saveDownlinkMsg(deviceCredentialsRequestMsg)); } @@ -256,21 +263,28 @@ public class EdgeImitator { return Futures.immediateFuture(null); } - public void waitForMessages() throws InterruptedException { - messagesLatch.await(5, TimeUnit.SECONDS); + public boolean waitForMessages() throws InterruptedException { + return waitForMessages(5); + } + + public boolean waitForMessages(int timeout) throws InterruptedException { + return messagesLatch.await(timeout, TimeUnit.SECONDS); } public void expectMessageAmount(int messageAmount) { messagesLatch = new CountDownLatch(messageAmount); } - public void waitForResponses() throws InterruptedException { responsesLatch.await(5, TimeUnit.SECONDS); } + public boolean waitForResponses() throws InterruptedException { + return responsesLatch.await(5, TimeUnit.SECONDS); + } public void expectResponsesAmount(int messageAmount) { responsesLatch = new CountDownLatch(messageAmount); } - public Optional findMessageByType(Class tClass) { + @SuppressWarnings("unchecked") + public Optional findMessageByType(Class tClass) { Optional result; try { lock.lock(); @@ -281,6 +295,18 @@ public class EdgeImitator { return result; } + @SuppressWarnings("unchecked") + public List findAllMessagesByType(Class tClass) { + List result; + try { + lock.lock(); + result = (List) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).collect(Collectors.toList()); + } finally { + lock.unlock(); + } + return result; + } + public AbstractMessage getLatestMessage() { return downlinkMsgs.get(downlinkMsgs.size() - 1); } diff --git a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java new file mode 100644 index 0000000000..0d470c78b9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.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.service; + +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.service.resource.*Test", + }) +public class ServiceSqlTestSuite { + + @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/schema-entities-idx.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/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java similarity index 88% rename from dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java rename to application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java index 21842af176..ffdefc27ed 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java @@ -13,28 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service; +package org.thingsboard.server.service.resource; import com.datastax.oss.driver.api.core.uuid.Uuids; 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.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.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.controller.AbstractControllerTest; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; -public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class BaseTbResourceServiceTest extends AbstractControllerTest { private static final String LWM2M_TEST_MODEL = "\n" + "\n" + @@ -67,18 +76,38 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { private TenantId tenantId; + @Autowired + private TbResourceService resourceService; + + private Tenant savedTenant; + private User tenantAdmin; + @Before - public void before() { + public void beforeTest() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); - Tenant savedTenant = tenantService.saveTenant(tenant); - Assert.assertNotNull(savedTenant); + 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 after() { - tenantService.deleteTenant(tenantId); + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); } @Test @@ -239,9 +268,10 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { @Test public void testFindTenantResourcesByTenantId() throws Exception { + loginSysAdmin(); Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); - tenant = tenantService.saveTenant(tenant); + tenant = doPost("/api/tenant", tenant, Tenant.class); TenantId tenantId = tenant.getId(); @@ -279,14 +309,17 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - tenantService.deleteTenant(tenantId); + doDelete("/api/tenant/" + tenantId.getId().toString()) + .andExpect(status().isOk()); } @Test public void testFindAllTenantResourcesByTenantId() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); - tenant = tenantService.saveTenant(tenant); + tenant = doPost("/api/tenant", tenant, Tenant.class); TenantId tenantId = tenant.getId(); @@ -344,7 +377,8 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - tenantService.deleteTenant(tenantId); + doDelete("/api/tenant/" + tenantId.getId().toString()) + .andExpect(status().isOk()); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java index 89ef53b19d..74523852a9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java @@ -15,35 +15,11 @@ */ package org.thingsboard.server.transport; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.junit.Assert; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceProfileProvisionType; -import org.thingsboard.server.common.data.DeviceProfileType; -import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; -import org.thingsboard.server.common.data.device.profile.DeviceProfileData; -import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; -import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.gen.transport.TransportProtos; @@ -51,8 +27,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -105,6 +79,22 @@ public abstract class AbstractTransportIntegrationTest extends AbstractControlle " }\n" + "}"; + protected static final String DEVICE_RPC_RESPONSE_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcResponseMsg {\n" + + " string payload = 1;\n" + + "}"; + + protected static final String DEVICE_RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " string params = 3;\n" + + "}"; + protected Tenant savedTenant; protected User tenantAdmin; diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java index ff64d70423..83023e21b9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java @@ -53,7 +53,7 @@ import static org.junit.Assert.assertNotNull; public abstract class AbstractCoapIntegrationTest extends AbstractTransportIntegrationTest { protected void processBeforeTest(String deviceName, CoapDeviceType coapDeviceType, TransportPayloadType payloadType) throws Exception { - this.processBeforeTest(deviceName, coapDeviceType, payloadType, null, null, DeviceProfileProvisionType.DISABLED, null, null); + this.processBeforeTest(deviceName, coapDeviceType, payloadType, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); } protected void processBeforeTest(String deviceName, @@ -61,8 +61,11 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg TransportPayloadType payloadType, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret + String rpcResponseProtoSchema, + String rpcRequestProtoSchema, + String provisionKey, + String provisionSecret, + DeviceProfileProvisionType provisionType ) throws Exception { loginSysAdmin(); @@ -85,7 +88,7 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg device.setType("default"); if (coapDeviceType != null) { - DeviceProfile coapDeviceProfile = createCoapDeviceProfile(payloadType, coapDeviceType, attributesProtoSchema, provisionType, provisionKey, provisionSecret, telemetryProtoSchema); + DeviceProfile coapDeviceProfile = createCoapDeviceProfile(payloadType, coapDeviceType, provisionSecret, provisionType, provisionKey, attributesProtoSchema, telemetryProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema); deviceProfile = doPost("/api/deviceProfile", coapDeviceProfile, DeviceProfile.class); device.setType(deviceProfile.getName()); device.setDeviceProfileId(deviceProfile.getId()); @@ -103,8 +106,9 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg } protected DeviceProfile createCoapDeviceProfile(TransportPayloadType transportPayloadType, CoapDeviceType coapDeviceType, - String attributesProtoSchema, DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret, String telemetryProtoSchema) { + String provisionSecret, DeviceProfileProvisionType provisionType, + String provisionKey, String attributesProtoSchema, + String telemetryProtoSchema, String rpcResponseProtoSchema, String rpcRequestProtoSchema) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -127,8 +131,16 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg if (StringUtils.isEmpty(attributesProtoSchema)) { attributesProtoSchema = DEVICE_ATTRIBUTES_PROTO_SCHEMA; } + if (StringUtils.isEmpty(rpcResponseProtoSchema)) { + rpcResponseProtoSchema = DEVICE_RPC_RESPONSE_PROTO_SCHEMA; + } + if (StringUtils.isEmpty(rpcRequestProtoSchema)) { + rpcRequestProtoSchema = DEVICE_RPC_REQUEST_PROTO_SCHEMA; + } protoTransportPayloadConfiguration.setDeviceTelemetryProtoSchema(telemetryProtoSchema); protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(attributesProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcResponseProtoSchema(rpcResponseProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcRequestProtoSchema(rpcRequestProtoSchema); transportPayloadTypeConfiguration = protoTransportPayloadConfiguration; } else { transportPayloadTypeConfiguration = new JsonTransportPayloadConfiguration(); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java index 4fbe103430..5bef44434e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java @@ -79,7 +79,7 @@ public abstract class AbstractCoapAttributesRequestProtoIntegrationTest extends @Test public void testRequestAttributesValuesFromTheServer() throws Exception { super.processBeforeTest("Test Request attribute values from the server proto", CoapDeviceType.DEFAULT, - TransportPayloadType.PROTOBUF, null, ATTRIBUTES_SCHEMA_STR, DeviceProfileProvisionType.DISABLED, null, null); + TransportPayloadType.PROTOBUF, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); processTestRequestAttributesValuesFromTheServer(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java index b4a55ae271..caecaea8ec 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java @@ -88,7 +88,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); @@ -97,7 +97,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -113,7 +113,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; byte[] result = createCoapClientAndPublish(requestCredentials).getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -132,7 +132,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; byte[] result = createCoapClientAndPublish(requestCredentials).getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -156,7 +156,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn } private void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -167,7 +167,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn } private void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java index 9f1d8f1f18..682d9c7e7b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java @@ -92,14 +92,14 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI private void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Assert.assertNotNull(result); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); } private void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); @@ -113,7 +113,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayload()); @@ -131,7 +131,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayload()); @@ -155,7 +155,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); @@ -165,7 +165,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index a36660267e..d5d56db63b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -124,7 +124,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC return COAP_BASE_URL + token + "/" + FeatureType.RPC.name().toLowerCase() + "/" + requestId; } - private class TestCoapCallback implements CoapHandler { + protected class TestCoapCallback implements CoapHandler { private final CoapClient client; private final CountDownLatch latch; @@ -136,7 +136,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC private Integer observe; - private TestCoapCallback(CoapClient client, CountDownLatch latch, boolean isOneWayRpc) { + TestCoapCallback(CoapClient client, CountDownLatch latch, boolean isOneWayRpc) { this.client = client; this.latch = latch; this.isOneWayRpc = isOneWayRpc; @@ -144,7 +144,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC @Override public void onLoad(CoapResponse response) { - log.warn("coap response: {}, {}", response, response.getCode()); + log.warn("coap response: {}, {}", response.getResponseText(), response.getCode()); assertNotNull(response.getPayload()); assertEquals(response.getCode(), CoAP.ResponseCode.CONTENT); observe = response.getOptions().getObserve(); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java index 5387d3c7bf..cecdb1fae0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java @@ -36,12 +36,12 @@ public abstract class AbstractCoapServerSideRpcJsonIntegrationTest extends Abstr } @Test - public void testServerMqttOneWayRpc() throws Exception { + public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(); } @Test - public void testServerMqttTwoWayRpc() throws Exception { + public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java index edbdf69848..706a560024 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java @@ -15,26 +15,62 @@ */ package org.thingsboard.server.transport.coap.rpc; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapHandler; +import org.eclipse.californium.core.CoapObserveRelation; import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Request; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.CoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractCoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSideRpcIntegrationTest { + private static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " Params params = 3;\n" + + "\n" + + " message Params {\n" + + " string pin = 1;\n" + + " int32 value = 2;\n" + + " }\n" + + "}"; + @Before public void beforeTest() throws Exception { - processBeforeTest("RPC test device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF); + processBeforeTest("RPC test device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED); } @After @@ -43,33 +79,91 @@ public abstract class AbstractCoapServerSideRpcProtoIntegrationTest extends Abst } @Test - public void testServerMqttOneWayRpc() throws Exception { + public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(); } @Test - public void testServerMqttTwoWayRpc() throws Exception { + public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest(); } + protected void processTwoWayRpcTest() throws Exception { + CoapClient client = getCoapClient(FeatureType.RPC); + client.useCONs(); + + CountDownLatch latch = new CountDownLatch(1); + TestCoapCallback testCoapCallback = new TestCoapCallback(client, latch, false); + + Request request = Request.newGet().setObserve(); + request.setType(CoAP.Type.CON); + CoapObserveRelation observeRelation = client.observe(request, testCoapCallback); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + + String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; + + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + latch.await(3, TimeUnit.SECONDS); + + assertEquals(expected, result); + assertEquals(0, testCoapCallback.getObserve().intValue()); + observeRelation.proactiveCancel(); + assertTrue(observeRelation.isCanceled()); + } + @Override protected void processOnLoadResponse(CoapResponse response, CoapClient client, Integer observe, CountDownLatch latch) { client.setURI(getRpcResponseFeatureTokenUrl(accessToken, observe)); - TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setPayload(DEVICE_RESPONSE) - .setRequestId(observe) - .build(); - client.post(new CoapHandler() { - @Override - public void onLoad(CoapResponse response) { - log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText()); - latch.countDown(); - } + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); + ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); + DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); - @Override - public void onError() { - log.warn("Command Response Ack Error, No connect"); + byte[] requestPayload = response.getPayload(); + DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); + Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); + assertNotNull(rpcRequestMsgDescriptor); + try { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); + List fields = rpcRequestMsgDescriptor.getFields(); + for (Descriptors.FieldDescriptor fieldDescriptor: fields) { + assertTrue(dynamicMessage.hasField(fieldDescriptor)); } - }, toDeviceRpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON); + ProtoFileElement rpcResponseProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcResponseProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); + DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); + Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); + assertNotNull(rpcResponseMsgDescriptor); + DynamicMessage rpcResponseMsg = rpcResponseBuilder + .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) + .build(); + client.post(new CoapHandler() { + @Override + public void onLoad(CoapResponse response) { + log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText()); + latch.countDown(); + } + + @Override + public void onError() { + log.warn("Command Response Ack Error, No connect"); + } + }, rpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON); + } catch (InvalidProtocolBufferException e) { + log.warn("Command Response Ack Error, Invalid response received: ", e); + } + } + + private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof CoapDeviceProfileTransportConfiguration); + CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; + CoapDeviceTypeConfiguration coapDeviceTypeConfiguration = coapDeviceProfileTransportConfiguration.getCoapDeviceTypeConfiguration(); + assertTrue(coapDeviceTypeConfiguration instanceof DefaultCoapDeviceTypeConfiguration); + DefaultCoapDeviceTypeConfiguration defaultCoapDeviceTypeConfiguration = (DefaultCoapDeviceTypeConfiguration) coapDeviceTypeConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = defaultCoapDeviceTypeConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; } } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java index 2ae686b211..0cc3db9cf1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java @@ -114,7 +114,7 @@ public abstract class AbstractCoapTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\n" + "}"; - super.processBeforeTest("Test Post Telemetry device proto payload", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, schemaStr, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof CoapDeviceProfileTransportConfiguration); CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index cd074b3197..0d11c9a773 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -62,7 +62,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protected DeviceProfile deviceProfile; protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { - this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, DeviceProfileProvisionType.DISABLED, null, null); + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); } protected void processBeforeTest(String deviceName, @@ -72,9 +72,12 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg String attributesTopic, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret - ) throws Exception { + String rpcResponseProtoSchema, + String rpcRequestProtoSchema, + String provisionKey, + String provisionSecret, + DeviceProfileProvisionType provisionType + ) throws Exception { loginSysAdmin(); Tenant tenant = new Tenant(); @@ -103,7 +106,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg gateway.setAdditionalInfo(additionalInfo); if (payloadType != null) { - DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, provisionType, provisionKey, provisionSecret); + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema, provisionKey, provisionSecret, provisionType); deviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); device.setType(deviceProfile.getName()); device.setDeviceProfileId(deviceProfile.getId()); @@ -157,8 +160,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret) { + String rpcResponseProtoSchema, String rpcRequestProtoSchema, + String provisionKey, String provisionSecret, + DeviceProfileProvisionType provisionType) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -186,8 +190,16 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg if (StringUtils.isEmpty(attributesProtoSchema)) { attributesProtoSchema = DEVICE_ATTRIBUTES_PROTO_SCHEMA; } + if (StringUtils.isEmpty(rpcResponseProtoSchema)) { + rpcResponseProtoSchema = DEVICE_RPC_RESPONSE_PROTO_SCHEMA; + } + if (StringUtils.isEmpty(rpcRequestProtoSchema)) { + rpcRequestProtoSchema = DEVICE_RPC_REQUEST_PROTO_SCHEMA; + } protoTransportPayloadConfiguration.setDeviceTelemetryProtoSchema(telemetryProtoSchema); protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(attributesProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcResponseProtoSchema(rpcResponseProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcRequestProtoSchema(rpcRequestProtoSchema); transportPayloadTypeConfiguration = protoTransportPayloadConfiguration; } mqttDeviceProfileTransportConfiguration.setTransportPayloadTypeConfiguration(transportPayloadTypeConfiguration); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java index ff8b54c79c..6949a544de 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java @@ -83,7 +83,7 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends @Test public void testRequestAttributesValuesFromTheServer() throws Exception { super.processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", - TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, DeviceProfileProvisionType.DISABLED, null, null); + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); processTestRequestAttributesValuesFromTheServer(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java index 106c801676..870b62b195 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -94,7 +94,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); @@ -103,7 +103,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -119,7 +119,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -138,7 +138,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -163,7 +163,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -188,7 +188,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -199,7 +199,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java index 53fde9412b..97ebbfe4a7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -101,14 +101,14 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertNotNull(result); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); } protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); @@ -122,7 +122,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes()); @@ -140,7 +140,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes()); @@ -164,7 +164,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateBasicMqttCredRequestMsg( ValidateBasicMqttCredRequestMsg.newBuilder() .setClientId("test_clientId") @@ -195,7 +195,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); @@ -205,7 +205,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 90de461627..f24cb0c2bc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -180,7 +180,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return message; } - private class TestMqttCallback implements MqttCallback { + protected class TestMqttCallback implements MqttCallback { private final MqttAsyncClient client; private final CountDownLatch latch; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java index 575dfdb052..eea800fb0e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java @@ -15,25 +15,60 @@ */ package org.thingsboard.server.transport.mqtt.rpc; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapHandler; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.jetbrains.annotations.NotNull; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportApiProtos; -import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + private static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " Params params = 3;\n" + + "\n" + + " message Params {\n" + + " string pin = 1;\n" + + " int32 value = 2;\n" + + " }\n" + + "}"; + @Before public void beforeTest() throws Exception { - processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null); + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED); } @After @@ -83,14 +118,55 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst return builder.build(); } + protected void processTwoWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; + latch.await(3, TimeUnit.SECONDS); + Assert.assertEquals(expected, result); + } + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { MqttMessage message = new MqttMessage(); if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { - TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setPayload(DEVICE_RESPONSE) - .setRequestId(0) + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); + ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); + DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); + + byte[] requestPayload = mqttMessage.getPayload(); + DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); + Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); + assertNotNull(rpcRequestMsgDescriptor); + try { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); + List fields = rpcRequestMsgDescriptor.getFields(); + for (Descriptors.FieldDescriptor fieldDescriptor: fields) { + assertTrue(dynamicMessage.hasField(fieldDescriptor)); + } + ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); + + DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); + Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); + assertNotNull(rpcResponseMsgDescriptor); + DynamicMessage rpcResponseMsg = rpcResponseBuilder + .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) .build(); - message.setPayload(toDeviceRpcResponseMsg.toByteArray()); + message.setPayload(rpcResponseMsg.toByteArray()); + } catch (InvalidProtocolBufferException e) { + log.warn("Command Response Ack Error, Invalid response received: ", e); + } } else { TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); String deviceName = msg.getDeviceName(); @@ -105,6 +181,14 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst return message; } + private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index 93a004318d..6140b7d8ed 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -119,7 +119,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\n" + "}"; - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); @@ -172,7 +172,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testPushMqttTelemetryGateway() throws Exception { - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); String deviceName1 = "Device A"; @@ -186,7 +186,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testGatewayConnect() throws Exception { - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); String deviceName = "Device A"; TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); 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 6639b75468..b37858ce36 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 @@ -102,4 +102,6 @@ public interface DeviceService { PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); + + long countByTenantId(TenantId tenantId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java index 2bcb0d3df0..ea25568375 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.event; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -41,6 +42,8 @@ public interface EventService { List findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit); + PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); + void removeEvents(TenantId tenantId, EntityId entityId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java new file mode 100644 index 0000000000..68c2c1f5f0 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.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.dao.resource; + +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +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; + +public interface ResourceService { + TbResource saveResource(TbResource resource); + + TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); + + TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); + + PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType lwm2mModel, String[] objectIds); + + PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink); + + void deleteResource(TenantId tenantId, TbResourceId resourceId); + + void deleteResourcesByTenantId(TenantId tenantId); + + +} 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 086d954e27..5740abb354 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 @@ -43,7 +43,7 @@ public interface RuleChainService { boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); - RuleChainMetaData saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); + boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId); diff --git a/common/data/pom.xml b/common/data/pom.xml index 15741da266..47902f493f 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -91,10 +91,6 @@ org.apache.commons commons-lang3 - - org.eclipse.leshan - leshan-core - 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 582df6a56a..854c5ff64b 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 @@ -19,14 +19,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; 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 org.thingsboard.server.common.data.validation.NoXss; +import javax.validation.Valid; import java.util.HashSet; import java.util.Set; public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { private TenantId tenantId; + @NoXss private String title; + @Valid private Set assignedCustomers; public DashboardInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index aa03cd7e59..d73dee6e1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.validation.NoXss; /** * Created by igor on 2/27/18. @@ -31,6 +32,7 @@ public class ShortCustomerInfo { private CustomerId customerId; @Getter @Setter + @NoXss private String title; @Getter @Setter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java new file mode 100644 index 0000000000..cbe7094715 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -0,0 +1,27 @@ +/** + * 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; + +public class StringUtils { + + public static boolean isEmpty(String source) { + return source == null || source.isEmpty(); + } + + public static boolean isNotEmpty(String source) { + return source != null && !source.isEmpty(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 33e94b2e0c..28c7e73e26 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @@ -27,6 +28,7 @@ public class TbResource extends TbResourceInfo { private static final long serialVersionUID = 7379609705527272306L; + @NoXss private String fileName; private String data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index d72982aeca..9414df3f78 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; public class TbResourceInfo extends SearchTextBased implements HasTenantId { private TenantId tenantId; + @NoXss private String title; private ResourceType resourceType; private String resourceKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 91bd902e9a..80e3730955 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -19,11 +19,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageLink; -import java.util.UUID; - /** * Created by ashvayka on 11.05.17. */ @@ -37,6 +34,5 @@ public class AlarmQuery { private AlarmSearchStatus searchStatus; private AlarmStatus status; private Boolean fetchOriginator; - private UUID idOffset; } 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 f5a438470e..f77e6388a5 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 @@ -25,6 +25,7 @@ import java.util.List; public class DeviceProfileData implements Serializable { private DeviceProfileConfiguration configuration; + @Valid private DeviceProfileTransportConfiguration transportConfiguration; private DeviceProfileProvisionConfiguration provisionConfiguration; @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index 3530cec583..63bb6324c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -17,11 +17,14 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @NoXss private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java index 13982f7e0b..159fbced16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.common.data.device.profile; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.os72.protobuf.dynamic.DynamicSchema; import com.github.os72.protobuf.dynamic.EnumDefinition; import com.github.os72.protobuf.dynamic.MessageDefinition; @@ -46,9 +45,13 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC public static final Location LOCATION = new Location("", "", -1, -1); public static final String ATTRIBUTES_PROTO_SCHEMA = "attributes proto schema"; public static final String TELEMETRY_PROTO_SCHEMA = "telemetry proto schema"; + public static final String RPC_RESPONSE_PROTO_SCHEMA = "rpc response proto schema"; + public static final String RPC_REQUEST_PROTO_SCHEMA = "rpc request proto schema"; private String deviceTelemetryProtoSchema; private String deviceAttributesProtoSchema; + private String deviceRpcRequestProtoSchema; + private String deviceRpcResponseProtoSchema; @Override public TransportPayloadType getTransportPayloadType() { @@ -63,13 +66,45 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC return getDescriptor(deviceAttributesProtoSchema, ATTRIBUTES_PROTO_SCHEMA); } + public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor(String deviceRpcResponseProtoSchema) { + return getDescriptor(deviceRpcResponseProtoSchema, RPC_RESPONSE_PROTO_SCHEMA); + } + + public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder(String deviceRpcRequestProtoSchema) { + return getDynamicMessageBuilder(deviceRpcRequestProtoSchema, RPC_REQUEST_PROTO_SCHEMA); + } + + public String getDeviceRpcResponseProtoSchema() { + if (!isEmptyStr(deviceRpcResponseProtoSchema)) { + return deviceRpcResponseProtoSchema; + } else { + return "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcResponseMsg {\n" + + " string payload = 1;\n" + + "}"; + } + } + + public String getDeviceRpcRequestProtoSchema() { + if (!isEmptyStr(deviceRpcRequestProtoSchema)) { + return deviceRpcRequestProtoSchema; + } else { + return "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " string params = 3;\n" + + "}"; + } + } + private Descriptors.Descriptor getDescriptor(String protoSchema, String schemaName) { try { - ProtoFileElement protoFileElement = getTransportProtoSchema(protoSchema); - DynamicSchema dynamicSchema = getDynamicSchema(protoFileElement, schemaName); - String lastMsgName = getMessageTypes(protoFileElement.getTypes()).stream() - .map(MessageElement::getName).reduce((previous, last) -> last).get(); - DynamicMessage.Builder builder = dynamicSchema.newMessageBuilder(lastMsgName); + DynamicMessage.Builder builder = getDynamicMessageBuilder(protoSchema, schemaName); return builder.getDescriptorForType(); } catch (Exception e) { log.warn("Failed to get Message Descriptor due to {}", e.getMessage()); @@ -77,6 +112,14 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC } } + public DynamicMessage.Builder getDynamicMessageBuilder(String protoSchema, String schemaName) { + ProtoFileElement protoFileElement = getTransportProtoSchema(protoSchema); + DynamicSchema dynamicSchema = getDynamicSchema(protoFileElement, schemaName); + String lastMsgName = getMessageTypes(protoFileElement.getTypes()).stream() + .map(MessageElement::getName).reduce((previous, last) -> last).get(); + return dynamicSchema.newMessageBuilder(lastMsgName); + } + public DynamicSchema getDynamicSchema(ProtoFileElement protoFileElement, String schemaName) { DynamicSchema.Builder schemaBuilder = DynamicSchema.newBuilder(); schemaBuilder.setName(schemaName); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java new file mode 100644 index 0000000000..a95949acbd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.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.common.data.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public abstract class DebugEvent implements EventFilter { + + private String msgDirectionType; + private String server; + private String dataSearch; + private String metadataSearch; + private String entityName; + private String relationType; + private String entityId; + private String msgType; + private boolean isError; + private String error; + + public void setIsError(boolean isError) { + this.isError = isError; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(msgDirectionType) || !StringUtils.isEmpty(server) || !StringUtils.isEmpty(dataSearch) || !StringUtils.isEmpty(metadataSearch) + || !StringUtils.isEmpty(entityName) || !StringUtils.isEmpty(relationType) || !StringUtils.isEmpty(entityId) || !StringUtils.isEmpty(msgType) || !StringUtils.isEmpty(error) || isError; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java new file mode 100644 index 0000000000..cc16d780f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.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.common.data.event; + +public class DebugRuleChainEventFilter extends DebugEvent { + @Override + public EventType getEventType() { + return EventType.DEBUG_RULE_CHAIN; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java new file mode 100644 index 0000000000..abe73e6e85 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.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.common.data.event; + +public class DebugRuleNodeEventFilter extends DebugEvent { + @Override + public EventType getEventType() { + return EventType.DEBUG_RULE_NODE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java new file mode 100644 index 0000000000..c9e9886c64 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java @@ -0,0 +1,36 @@ +/** + * 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class ErrorEventFilter implements EventFilter { + private String server; + private String method; + private String error; + + @Override + public EventType getEventType() { + return EventType.ERROR; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(method) || !StringUtils.isEmpty(error); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java new file mode 100644 index 0000000000..eebab7e7d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.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.event; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "eventType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DebugRuleNodeEventFilter.class, name = "DEBUG_RULE_NODE"), + @JsonSubTypes.Type(value = DebugRuleChainEventFilter.class, name = "DEBUG_RULE_CHAIN"), + @JsonSubTypes.Type(value = ErrorEventFilter.class, name = "ERROR"), + @JsonSubTypes.Type(value = LifeCycleEventFilter.class, name = "LC_EVENT"), + @JsonSubTypes.Type(value = StatisticsEventFilter.class, name = "STATS") +}) +public interface EventFilter { + @JsonIgnore + EventType getEventType(); + + boolean hasFilterForJsonBody(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.java new file mode 100644 index 0000000000..a0700fdfbc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.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.event; + +public enum EventType { + ERROR, LC_EVENT, STATS, DEBUG_RULE_NODE, DEBUG_RULE_CHAIN +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java new file mode 100644 index 0000000000..a871ccb106 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.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.server.common.data.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class LifeCycleEventFilter implements EventFilter { + private String server; + private String event; + private String status; + private String error; + + @Override + public EventType getEventType() { + return EventType.LC_EVENT; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(event) || !StringUtils.isEmpty(status) || !StringUtils.isEmpty(error); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java new file mode 100644 index 0000000000..9fad8b4bca --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java @@ -0,0 +1,36 @@ +/** + * 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class StatisticsEventFilter implements EventFilter { + private String server; + private Integer messagesProcessed; + private Integer errorsOccurred; + + @Override + public EventType getEventType() { + return EventType.STATS; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || (messagesProcessed != null && messagesProcessed > 0) || (errorsOccurred != null && errorsOccurred > 0); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index fc63133510..72713f12d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.tenant.profile; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.TenantProfileType; +@AllArgsConstructor +@NoArgsConstructor +@Builder @Data public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index e3463f3614..300015fa23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class BaseWidgetType extends BaseData implements HasTenantId { @@ -27,8 +28,11 @@ public class BaseWidgetType extends BaseData implements HasTenantI private static final long serialVersionUID = 8388684344603660756L; private TenantId tenantId; + @NoXss private String bundleAlias; + @NoXss private String alias; + @NoXss private String name; public BaseWidgetType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 232a10e755..c43d59041c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -15,14 +15,15 @@ */ package org.thingsboard.server.common.data.widget; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeDetails extends WidgetType { private String image; + @NoXss private String description; public WidgetTypeDetails() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java index 645483bcc3..2fde13552b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java @@ -17,12 +17,15 @@ package org.thingsboard.server.common.data.widget; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeInfo extends BaseWidgetType { private String image; + @NoXss private String description; + @NoXss private String widgetType; public WidgetTypeInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index a15cd8baf5..df09f75922 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.Arrays; @@ -27,9 +28,12 @@ public class WidgetsBundle extends SearchTextBased implements H private static final long serialVersionUID = -7627368878362410489L; private TenantId tenantId; + @NoXss private String alias; + @NoXss private String title; private String image; + @NoXss private String description; public WidgetsBundle() { diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e0b30dd654..31ec44e8f7 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -112,6 +112,10 @@ com.google.protobuf protobuf-java + + com.google.protobuf + protobuf-java-util + org.apache.curator curator-recipes diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 6bd2a69b2b..4481e52461 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -21,22 +21,28 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; +import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Queue; import java.util.Set; -import java.util.concurrent.locks.Lock; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; + @Slf4j public abstract class AbstractTbQueueConsumerTemplate implements TbQueueConsumer { private volatile boolean subscribed; protected volatile boolean stopped = false; protected volatile Set partitions; - protected final Lock consumerLock = new ReentrantLock(); + protected final ReentrantLock consumerLock = new ReentrantLock(); //NonfairSync + final Queue> subscribeQueue = new ConcurrentLinkedQueue<>(); @Getter private final String topic; @@ -47,84 +53,101 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void subscribe() { - consumerLock.lock(); - try { - partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true)); - subscribed = false; - } finally { - consumerLock.unlock(); + log.info("enqueue topic subscribe {} ", topic); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; } + subscribeQueue.add(Collections.singleton(new TopicPartitionInfo(topic, null, null, true))); } @Override public void subscribe(Set partitions) { + log.info("enqueue topics subscribe {} ", partitions); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; + } + subscribeQueue.add(partitions); + } + + @Override + public List poll(long durationInMillis) { + List records; + long startNanos = System.nanoTime(); + if (stopped) { + return errorAndReturnEmpty(); + } + if (!subscribed && partitions == null && subscribeQueue.isEmpty()) { + return sleepAndReturnEmpty(startNanos, durationInMillis); + } + + if (consumerLock.isLocked()) { + log.error("poll. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock", new RuntimeException("stacktrace")); + } + consumerLock.lock(); try { - this.partitions = partitions; - subscribed = false; + while (!subscribeQueue.isEmpty()) { + subscribed = false; + partitions = subscribeQueue.poll(); + } + if (!subscribed) { + List topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + doSubscribe(topicNames); + subscribed = true; + } + records = partitions.isEmpty() ? emptyList() : doPoll(durationInMillis); } finally { consumerLock.unlock(); } + + if (records.isEmpty()) { return sleepAndReturnEmpty(startNanos, durationInMillis); } + + return decodeRecords(records); } - @Override - public List poll(long durationInMillis) { - if (!subscribed && partitions == null) { - try { - Thread.sleep(durationInMillis); - } catch (InterruptedException e) { - log.debug("Failed to await subscription", e); - } - } else { - long pollStartTs = System.currentTimeMillis(); - consumerLock.lock(); + @Nonnull + List decodeRecords(@Nonnull List records) { + List result = new ArrayList<>(records.size()); + records.forEach(record -> { try { - if (!subscribed) { - List topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); - doSubscribe(topicNames); - subscribed = true; + if (record != null) { + result.add(decode(record)); } + } catch (IOException e) { + log.error("Failed decode record: [{}]", record); + throw new RuntimeException("Failed to decode record: ", e); + } + }); + return result; + } - List records; - if (partitions.isEmpty()) { - records = Collections.emptyList(); - } else { - records = doPoll(durationInMillis); - } - if (!records.isEmpty()) { - List result = new ArrayList<>(records.size()); - records.forEach(record -> { - try { - if (record != null) { - result.add(decode(record)); - } - } catch (IOException e) { - log.error("Failed decode record: [{}]", record); - throw new RuntimeException("Failed to decode record: ", e); - } - }); - return result; - } else { - long pollDuration = System.currentTimeMillis() - pollStartTs; - if (pollDuration < durationInMillis) { - try { - Thread.sleep(durationInMillis - pollDuration); - } catch (InterruptedException e) { - if (!stopped) { - log.error("Failed to wait.", e); - } - } - } + List errorAndReturnEmpty() { + log.error("poll invoked but consumer stopped for topic" + topic, new RuntimeException("stacktrace")); + return emptyList(); + } + + List sleepAndReturnEmpty(final long startNanos, final long durationInMillis) { + long durationNanos = TimeUnit.MILLISECONDS.toNanos(durationInMillis); + long spentNanos = System.nanoTime() - startNanos; + if (spentNanos < durationNanos) { + try { + Thread.sleep(Math.max(TimeUnit.NANOSECONDS.toMillis(durationNanos - spentNanos), 1)); + } catch (InterruptedException e) { + if (!stopped) { + log.error("Failed to wait", e); } - } finally { - consumerLock.unlock(); } } - return Collections.emptyList(); + return emptyList(); } @Override public void commit() { + if (consumerLock.isLocked()) { + log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock", new RuntimeException("stacktrace")); + } consumerLock.lock(); try { doCommit(); @@ -135,6 +158,7 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void unsubscribe() { + log.info("unsubscribe topic and stop consumer {}", getTopic()); stopped = true; consumerLock.lock(); try { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index 0802f0f52c..bfe289a5e8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -128,9 +128,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi log.debug("Ignoring application ready event, ZK client is not started, ZK client state [{}]", client.getState()); return; } + log.info("Going to publish current server..."); publishCurrentServer(); - TransportProtos.ServiceInfo currentService = serviceInfoProvider.getServiceInfo(); - partitionService.recalculatePartitions(currentService, getOtherServers()); + log.info("Going to recalculate partitions..."); + recalculatePartitions(); } public synchronized void publishCurrentServer() { @@ -285,11 +286,19 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi case CHILD_ADDED: case CHILD_UPDATED: case CHILD_REMOVED: - TransportProtos.ServiceInfo currentService = serviceInfoProvider.getServiceInfo(); - partitionService.recalculatePartitions(currentService, getOtherServers()); + recalculatePartitions(); break; default: break; } } + + /** + * A single entry point to recalculate partitions + * Synchronized to ensure that other servers info is up to date + * */ + synchronized void recalculatePartitions() { + partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index b1b3d5ce05..4429f3518f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -72,8 +72,10 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue protected void doSubscribe(List topicNames) { if (!topicNames.isEmpty()) { topicNames.forEach(admin::createTopicIfNotExists); + log.info("subscribe topics {}", topicNames); consumer.subscribe(topicNames); } else { + log.info("unsubscribe due to empty topic list"); consumer.unsubscribe(); } } @@ -102,6 +104,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue @Override protected void doUnsubscribe() { + log.info("unsubscribe topic and close consumer for topic {}", getTopic()); if (consumer != null) { consumer.unsubscribe(); consumer.close(); 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 8d756631e8..dcbb62e0c5 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.coap; import com.google.gson.JsonParseException; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; @@ -261,7 +262,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); if (currentAttrSession == null) { attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); } @@ -281,7 +283,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); if (currentRpcSession == null) { rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); @@ -303,17 +306,19 @@ public class CoapTransportResource extends AbstractCoapTransportResource { break; case TO_DEVICE_RPC_RESPONSE: transportService.process(sessionInfo, - coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), + coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request, transportConfigurationContainer.getRpcResponseMsgDescriptor()), 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.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout); transportService.process(sessionInfo, coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), new CoapNoOpCallback(exchange)); break; case GET_ATTRIBUTES_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), timeout); + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout); transportService.process(sessionInfo, coapTransportAdaptor.convertToGetAttributes(sessionId, request), new CoapNoOpCallback(exchange)); @@ -330,14 +335,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return tokenToSessionIdMap.remove(token); } - private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, String token) { + private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, String token) { tokenToSessionIdMap.putIfAbsent(token, sessionInfo); - transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor)); + transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder)); transportService.process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null); } - private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor) { - return new CoapSessionListener(exchange, coapTransportAdaptor); + private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { + return new CoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder); } private String getTokenFromRequest(Request request) { @@ -423,10 +428,12 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private final CoapExchange exchange; private final CoapTransportAdaptor coapTransportAdaptor; + private final DynamicMessage.Builder rpcRequestDynamicMessageBuilder; - CoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor) { + CoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { this.exchange = exchange; this.coapTransportAdaptor = coapTransportAdaptor; + this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder; } @Override @@ -457,7 +464,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg msg) { try { - exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg)); + exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder)); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); @@ -545,9 +552,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; String deviceTelemetryProtoSchema = protoTransportPayloadConfiguration.getDeviceTelemetryProtoSchema(); String deviceAttributesProtoSchema = protoTransportPayloadConfiguration.getDeviceAttributesProtoSchema(); + String deviceRpcRequestProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema(); + String deviceRpcResponseProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcResponseProtoSchema(); return new TransportConfigurationContainer(false, protoTransportPayloadConfiguration.getTelemetryDynamicMessageDescriptor(deviceTelemetryProtoSchema), - protoTransportPayloadConfiguration.getAttributesDynamicMessageDescriptor(deviceAttributesProtoSchema)); + protoTransportPayloadConfiguration.getAttributesDynamicMessageDescriptor(deviceAttributesProtoSchema), + protoTransportPayloadConfiguration.getRpcResponseDynamicMessageDescriptor(deviceRpcResponseProtoSchema), + protoTransportPayloadConfiguration.getRpcRequestDynamicMessageBuilder(deviceRpcRequestProtoSchema) + ); } } else { throw new AdaptorException("Invalid CoapDeviceTypeConfiguration type: " + coapDeviceTypeConfiguration.getClass().getSimpleName() + "!"); @@ -567,11 +579,15 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private boolean jsonPayload; private Descriptors.Descriptor telemetryMsgDescriptor; private Descriptors.Descriptor attributesMsgDescriptor; + private Descriptors.Descriptor rpcResponseMsgDescriptor; + private DynamicMessage.Builder rpcRequestDynamicMessageBuilder; - public TransportConfigurationContainer(boolean jsonPayload, Descriptors.Descriptor telemetryMsgDescriptor, Descriptors.Descriptor attributesMsgDescriptor) { + public TransportConfigurationContainer(boolean jsonPayload, Descriptors.Descriptor telemetryMsgDescriptor, Descriptors.Descriptor attributesMsgDescriptor, Descriptors.Descriptor rpcResponseMsgDescriptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { this.jsonPayload = jsonPayload; this.telemetryMsgDescriptor = telemetryMsgDescriptor; this.attributesMsgDescriptor = attributesMsgDescriptor; + this.rpcResponseMsgDescriptor = rpcResponseMsgDescriptor; + this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder; } public TransportConfigurationContainer(boolean jsonPayload) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java index 1d4703ead4..b30c86307b 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.coap.adaptors; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.thingsboard.server.common.transport.adaptor.AdaptorException; @@ -32,7 +33,7 @@ public interface CoapTransportAdaptor { TransportProtos.GetAttributeRequestMsg convertToGetAttributes(UUID sessionId, Request inbound) throws AdaptorException; - TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException; + TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException; TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException; @@ -42,7 +43,7 @@ public interface CoapTransportAdaptor { Response convertToPublish(boolean isConfirmable, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; + Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException; Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index e382b2a3ee..85d35301c3 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -20,6 +20,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; @@ -64,7 +65,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException { + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); String payload = validatePayload(sessionId, inbound, false); JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); @@ -95,7 +96,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg) throws AdaptorException { + public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { return getObserveNotification(isConfirmable, JsonConverter.toJson(msg, true)); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index d4c53fdc9a..120e49f697 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.coap.adaptors; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; @@ -63,16 +64,16 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException { + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); if (requestId.isEmpty()) { throw new AdaptorException("Request id is missing!"); } else { try { - String payload = TransportProtos.ToDeviceRpcResponseMsg.parseFrom(inbound.getPayload()).getPayload(); - return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.get()) - .setPayload(payload).build(); - } catch (InvalidProtocolBufferException e) { + JsonElement response = new JsonParser().parse(dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) + .setPayload(response.toString()).build(); + } catch (Exception e) { throw new AdaptorException(e); } } @@ -112,8 +113,8 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg) throws AdaptorException { - return getObserveNotification(isConfirmable, msg.toByteArray()); + public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + return getObserveNotification(isConfirmable, ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder)); } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java index ee2b3bac20..358ba499d4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java @@ -35,12 +35,11 @@ import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInf import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContextServer; -import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; import org.thingsboard.server.transport.lwm2m.utils.TypeServer; import java.io.IOException; import java.security.GeneralSecurityException; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -70,8 +69,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @Override public List getAllByEndpoint(String endPoint) { - String endPointKey = endPoint; - ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP); + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPoint, TypeServer.BOOTSTRAP); if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { /** add value to store from BootstrapJson */ this.setBootstrapConfigScurityInfo(store); @@ -87,7 +85,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { } catch (InvalidConfigurationException e) { log.error("", e); } - return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo()); + return store.getSecurityInfo() == null ? null : Collections.singletonList(store.getSecurityInfo()); } } return null; @@ -166,13 +164,13 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap); lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer); String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndPoint()); - context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboardTelemetry(context.getKvLogyToThingsboard(logMsg), sessionInfo); return lwM2MBootstrapConfig; } else { log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndPoint()); log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); - context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboardTelemetry(context.getKvLogyToThingsboard(logMsg), sessionInfo); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java index e3757701cb..b08f1dbcbe 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java @@ -23,5 +23,5 @@ public class LwM2MBootstrapServers { private Integer lifetime = 300; private Integer defaultMinPeriod = 1; private boolean notifIfDisabled = true; - private String binding = "U"; + private String binding = "UQ"; } diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java similarity index 84% rename from ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java index 97c400525e..dfc88a3331 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.thingsboard.server.transport.lwm2m.server; -:host { - min-width: 364px; +public interface LwM2mQueuedRequest { + void send(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index ffe2f76602..9b2f3cb7eb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -26,7 +26,7 @@ import org.eclipse.leshan.server.registration.RegistrationUpdate; import java.util.Collection; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; @Slf4j public class LwM2mServerListener { @@ -92,7 +92,7 @@ public class LwM2mServerListener { public void onResponse(Observation observation, Registration registration, ObserveResponse response) { if (registration != null) { try { - service.onObservationResponse(registration, convertToIdVerFromObjectId(observation.getPath().toString(), registration), response); + service.onObservationResponse(registration, convertPathFromObjectIdToIdVer(observation.getPath().toString(), registration), response); } catch (Exception e) { log.error("[{}] onResponse", e.toString()); @@ -107,7 +107,7 @@ public class LwM2mServerListener { @Override public void newObservation(Observation observation, Registration registration) { -// log.info("Received newObservation from [{}] endpoint [{}] ", observation.getPath(), registration.getEndpoint()); + log.info("Received newObservation from [{}] endpoint [{}] ", observation.getPath(), registration.getEndpoint()); } }; 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 20baeba453..0bc78254a6 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 @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.ResourceType; @@ -74,12 +75,12 @@ public class LwM2mSessionMsgListener implements GenericFutureListener resourceUpdateMsgOpt) { + @Override + public void onResourceUpdate(@NotNull Optional resourceUpdateMsgOpt) { if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.get().getResourceType())) { this.service.onResourceUpdate(resourceUpdateMsgOpt); } } - public void onResourceDelete(Optional resourceDeleteMsgOpt) { + @Override + public void onResourceDelete(@NotNull 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/LwM2mTransportContextServer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java index 170530f084..ed0a1d16e6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java @@ -30,31 +30,33 @@ package org.thingsboard.server.transport.lwm2m.server; * limitations under the License. */ -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.model.DDFFileParser; import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.codec.CodecException; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportResourceCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigServer; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.adaptors.LwM2MJsonAdaptor; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.BOOLEAN_V; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; @Slf4j @@ -91,7 +93,7 @@ public class LwM2mTransportContextServer extends TransportContext { /** * send to Thingsboard Attribute || Telemetry * - * @param msg - JsonObject: [{name: value}] + * @param msg - JsonObject: [{name: value}] * @return - dummy */ private TransportServiceCallback getPubAckCallbackSendAttrTelemetry(final T msg) { @@ -108,33 +110,29 @@ public class LwM2mTransportContextServer extends TransportContext { }; } - public void sendParametersOnThingsboard(JsonElement msg, String topicName, SessionInfoProto sessionInfo) { - try { - if (topicName.equals(LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC)) { - PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(msg); - TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg); - transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call)); - } else if (topicName.equals(LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC)) { - PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(msg); - TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg); - transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call)); - } - } catch (AdaptorException e) { - log.error("[{}] Failed to process publish msg [{}]", topicName, e); - log.info("[{}] Closing current session due to invalid publish", topicName); - } + public void sendParametersOnThingsboardAttribute(List result, SessionInfoProto sessionInfo) { + PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder(); + request.addAllKv(result); + PostAttributeMsg postAttributeMsg = request.build(); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg); + transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } - public JsonObject getTelemetryMsgObject(String logMsg) { - JsonObject telemetries = new JsonObject(); - telemetries.addProperty(LOG_LW2M_TELEMETRY, logMsg); - return telemetries; + public void sendParametersOnThingsboardTelemetry(List result, SessionInfoProto sessionInfo) { + PostTelemetryMsg.Builder request = PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.setTs(System.currentTimeMillis()); + builder.addAllKv(result); + request.addTsKvList(builder.build()); + PostTelemetryMsg postTelemetryMsg = request.build(); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg); + transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } /** * @return - sessionInfo after access connect client */ - public SessionInfoProto getValidateSessionInfo(ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { + public SessionInfoProto getValidateSessionInfo(TransportProtos.ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { return SessionInfoProto.newBuilder() .setNodeId(this.getNodeId()) .setSessionIdMSB(mostSignificantBits) @@ -159,4 +157,90 @@ public class LwM2mTransportContextServer extends TransportContext { return null; } } + + /** + * + * @param logMsg - info about Logs + * @return- KeyValueProto for telemetry (Logs) + */ + public List getKvLogyToThingsboard(String logMsg) { + List result = new ArrayList<>(); + result.add(TransportProtos.KeyValueProto.newBuilder() + .setKey(LOG_LW2M_TELEMETRY) + .setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(logMsg).build()); + return result; + } + + /** + * @return - KeyValueProto for attribute/telemetry (change value) + * @throws CodecException - + */ + + public TransportProtos.KeyValueProto getKvAttrTelemetryToThingsboard(ResourceModel.Type resourceType, String resourceName, Object value, boolean isMultiInstances) { + TransportProtos.KeyValueProto.Builder kvProto = TransportProtos.KeyValueProto.newBuilder().setKey(resourceName); + if (isMultiInstances) { + kvProto.setType(TransportProtos.KeyValueType.JSON_V) + .setJsonV((String) value); + } + else { + switch (resourceType) { + case BOOLEAN: + kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build(); + break; + case STRING: + case TIME: + case OPAQUE: + case OBJLNK: + kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value); + break; + case INTEGER: + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV((Long) value); + break; + case FLOAT: + kvProto.setType(TransportProtos.KeyValueType.DOUBLE_V).setDoubleV((Double) value); + } + } + return kvProto.build(); + } + + /** + * + * @param currentType - + * @param resourcePath - + * @return + */ + public ResourceModel.Type getResourceModelTypeEqualsKvProtoValueType(ResourceModel.Type currentType, String resourcePath) { + switch (currentType) { + case BOOLEAN: + return ResourceModel.Type.BOOLEAN; + case STRING: + case TIME: + case OPAQUE: + case OBJLNK: + return ResourceModel.Type.STRING; + case INTEGER: + return ResourceModel.Type.INTEGER; + case FLOAT: + return ResourceModel.Type.FLOAT; + default: + } + throw new CodecException("Invalid ResourceModel_Type for resource %s, got %s", resourcePath, currentType); + } + + public Object getValueFromKvProto (TransportProtos.KeyValueProto kv) { + switch (kv.getType()) { + case BOOLEAN_V: + return kv.getBoolV(); + case LONG_V: + return kv.getLongV(); + case DOUBLE_V: + return kv.getDoubleV(); + case STRING_V: + return kv.getStringV(); + case JSON_V: + return kv.getJsonV(); + } + return null; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java index 0f0707ddf8..86dd5b7960 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java @@ -22,15 +22,14 @@ import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.core.attributes.Attribute; +import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; -import org.eclipse.leshan.core.node.LwM2mMultipleResource; -import org.eclipse.leshan.core.node.LwM2mNode; -import org.eclipse.leshan.core.node.LwM2mObject; -import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.registration.Registration; @@ -44,11 +43,19 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import static org.eclipse.leshan.core.attributes.Attribute.DIMENSION; +import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD; +import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; +import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @@ -56,8 +63,21 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA public class LwM2mTransportHandler { public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; + + public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; + public static final String BOOTSTRAP = "bootstrap"; + public static final String SERVERS = "servers"; + public static final String LWM2M_SERVER = "lwm2mServer"; + public static final String BOOTSTRAP_SERVER = "bootstrapServer"; + public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr"; public static final String ATTRIBUTE = "attribute"; public static final String TELEMETRY = "telemetry"; + public static final String KEY_NAME = "keyName"; + public static final String OBSERVE = "observe"; + public static final String ATTRIBUTE_LWM2M = "attributeLwm2m"; + public static final String RESOURCE_VALUE = "resValue"; + public static final String RESOURCE_TYPE = "resType"; + private static final String REQUEST = "/request"; private static final String RESPONSE = "/response"; private static final String ATTRIBUTES = "/" + ATTRIBUTE; @@ -70,14 +90,6 @@ public class LwM2mTransportHandler { public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRIES; public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms - public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr"; - public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; - public static final String KEY_NAME = "keyName"; - public static final String OBSERVE = "observe"; - public static final String BOOTSTRAP = "bootstrap"; - public static final String SERVERS = "servers"; - public static final String LWM2M_SERVER = "lwm2mServer"; - public static final String BOOTSTRAP_SERVER = "bootstrapServer"; public static final String LOG_LW2M_TELEMETRY = "logLwm2m"; public static final String LOG_LW2M_INFO = "info"; @@ -144,19 +156,19 @@ public class LwM2mTransportHandler { throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type); } } - - public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { - if (content instanceof LwM2mObject) { - return (LwM2mObject) content; - } else if (content instanceof LwM2mObjectInstance) { - return (LwM2mObjectInstance) content; - } else if (content instanceof LwM2mSingleResource) { - return (LwM2mSingleResource) content; - } else if (content instanceof LwM2mMultipleResource) { - return (LwM2mMultipleResource) content; - } - return null; - } +// +// public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { +// if (content instanceof LwM2mObject) { +// return (LwM2mObject) content; +// } else if (content instanceof LwM2mObjectInstance) { +// return (LwM2mObjectInstance) content; +// } else if (content instanceof LwM2mSingleResource) { +// return (LwM2mSingleResource) content; +// } else if (content instanceof LwM2mMultipleResource) { +// return (LwM2mMultipleResource) content; +// } +// return null; +// } public static LwM2mClientProfile getNewProfileParameters(JsonObject profilesConfigData, TenantId tenantId) { LwM2mClientProfile lwM2MClientProfile = new LwM2mClientProfile(); @@ -166,6 +178,7 @@ public class LwM2mTransportHandler { lwM2MClientProfile.setPostAttributeProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).getAsJsonArray()); lwM2MClientProfile.setPostTelemetryProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).getAsJsonArray()); lwM2MClientProfile.setPostObserveProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).getAsJsonArray()); + lwM2MClientProfile.setPostAttributeLwm2mProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).getAsJsonObject()); return lwM2MClientProfile; } @@ -184,6 +197,9 @@ public class LwM2mTransportHandler { * "attribute":["/2/0/1","/3/0/9"], * "telemetry":["/1/0/1","/2/0/1","/6/0/1"], * "observe":["/2/0","/2/0/0","/4/0/2"]} + * "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"}, + * "/3_1.0/0": {"gt": 17}, + * "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}} */ public static LwM2mClientProfile getLwM2MClientProfileFromThingsboard(DeviceProfile deviceProfile) { if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { @@ -192,7 +208,7 @@ public class LwM2mTransportHandler { ObjectMapper mapper = new ObjectMapper(); String profileStr = mapper.writeValueAsString(profile); JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null; - return (getValidateCredentialsBodyFromThingsboard(profileJson)) ? LwM2mTransportHandler.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null; + return getValidateCredentialsBodyFromThingsboard(profileJson) ? LwM2mTransportHandler.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null; } catch (IOException e) { log.error("", e); } @@ -240,7 +256,10 @@ public class LwM2mTransportHandler { objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonArray() && objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(OBSERVE) && !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonNull() && - objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonArray()); + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonArray() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(ATTRIBUTE_LWM2M) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonObject()); } private static boolean getValidateBootstrapProfileFromThingsboard(JsonObject objectMsg) { @@ -316,28 +335,30 @@ public class LwM2mTransportHandler { }; } - public static String convertToObjectIdFromIdVer(String key) { + public static String convertPathFromIdVerToObjectId(String pathIdVer) { try { - String[] keyArray = key.split(LWM2M_SEPARATOR_PATH); + String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH); if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) { keyArray[1] = keyArray[1].split(LWM2M_SEPARATOR_KEY)[0]; return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); - } else { - return key; + } + else { + return pathIdVer; } } catch (Exception e) { return null; } } - public static String convertToIdVerFromObjectId(String path, Registration registration) { + public static String convertPathFromObjectIdToIdVer(String path, Registration registration) { String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()); try { String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); if (keyArray.length > 1) { keyArray[1] = keyArray[1] + LWM2M_SEPARATOR_KEY + ver; return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); - } else { + } + else { return path; } } catch (Exception e) { @@ -345,14 +366,6 @@ public class LwM2mTransportHandler { } } - public static Integer validateObjectIdFromKey(String key) { - try { - return Integer.parseInt(key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[0]); - } catch (Exception e) { - return null; - } - } - public static String validateObjectVerFromKey(String key) { try { return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]); @@ -360,4 +373,54 @@ public class LwM2mTransportHandler { return ObjectModel.DEFAULT_VERSION; } } + + /** + * As example: + * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified + * to the Server with a minimum interval of 1sec; + * this value is set at theResource level. + * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified + * to the Server with a minimum value (pmin) given by the default one + * (resource 2 of Object Server ID=1), + * or with another value if this Attribute has been set at another level + * (Object or Object Instance: see section5.1.1). + * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ + * will be notified to the Server with a minimum interval of 10 sec; + * this value is set at the Object Instance level. + * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 20 and new value is 35 due to step condition + * b.old value is 45 and new value is 50 due to gt condition + * c.old value is 50 and new value is 40 due to both gt and step conditions + * d.old value is 35 and new value is 20 due to step conditione) + * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 17 and new value is 24 due to lt condition + * b.old value is 75 and new value is 90 due to both gt and step conditions + * String uriQueries = "pmin=10&pmax=60"; + * AttributeSet attributes = AttributeSet.parse(uriQueries); + * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); + * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); + * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); + * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); + * Attribute [] attrs = {gt, st}; + */ + public static DownlinkRequest createWriteAttributeRequest(String target, Object params) { + AttributeSet attrSet = new AttributeSet(createWriteAttributes(params)); + return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null; + } + + private static Attribute[] createWriteAttributes(Object params) { + List attributeLists = new ArrayList(); + ObjectMapper oMapper = new ObjectMapper(); + Map map = oMapper.convertValue(params, ConcurrentHashMap.class); + map.forEach((k, v) -> { + if (!v.toString().isEmpty() || (v.toString().isEmpty() && OBJECT_VERSION.equals(k))) { + attributeLists.add(new Attribute(k, + (DIMENSION.equals(k) || MINIMUM_PERIOD.equals(k) || MAXIMUM_PERIOD.equals(k)) ? + ((Double) v).longValue() : v)); + } + }); + return (Attribute[]) attributeLists.toArray(Attribute[]::new); + } } 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 a879e4bc78..d717c90418 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 @@ -16,9 +16,8 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Response; -import org.eclipse.leshan.core.attributes.Attribute; -import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; @@ -32,8 +31,8 @@ import org.eclipse.leshan.core.request.DownlinkRequest; import org.eclipse.leshan.core.request.ExecuteRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; -import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.exception.ClientSleepingException; import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.DeleteResponse; import org.eclipse.leshan.core.response.DiscoverResponse; @@ -58,8 +57,6 @@ import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess; -import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; @@ -72,8 +69,9 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandle import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_UPDATE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.RESPONSE_CHANNEL; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.createWriteAttributeRequest; @Slf4j @Service @@ -116,13 +114,13 @@ public class LwM2mTransportRequest { */ public void sendAllRequest(Registration registration, String targetIdVer, String typeOper, String contentFormatParam, Observation observation, Object params, long timeoutInMs) { - String target = convertToObjectIdFromIdVer(targetIdVer); + String target = convertPathFromIdVerToObjectId(targetIdVer); LwM2mPath resultIds = new LwM2mPath(target); if (registration != null && resultIds.getObjectId() >= 0) { DownlinkRequest request = null; ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null; LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); - ResourceModel resource = lwM2MClient.getResourceModel(targetIdVer); + ResourceModel resource = null; timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; switch (typeOper) { case GET_TYPE_OPER_READ: @@ -144,6 +142,7 @@ public class LwM2mTransportRequest { request = new CancelObservationRequest(observation); break; case POST_TYPE_OPER_EXECUTE: + resource = lwM2MClient.getResourceModel(targetIdVer); if (params != null && resource != null && !resource.multiple) { request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resource.type, ResourceModel.Type.STRING, resultIds)); } else { @@ -152,6 +151,7 @@ public class LwM2mTransportRequest { break; case POST_TYPE_OPER_WRITE_REPLACE: // Request to write a String Single-Instance Resource using the TLV content format. + resource = lwM2MClient.getResourceModel(targetIdVer); if (resource != null && contentFormat != null) { // if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { if (contentFormat.equals(ContentFormat.TLV)) { @@ -173,55 +173,21 @@ public class LwM2mTransportRequest { } break; case PUT_TYPE_OPER_WRITE_ATTRIBUTES: - /** - * As example: - * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified - * to the Server with a minimum interval of 1sec; - * this value is set at theResource level. - * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified - * to the Server with a minimum value (pmin) given by the default one - * (resource 2 of Object Server ID=1), - * or with another value if this Attribute has been set at another level - * (Object or Object Instance: see section5.1.1). - * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ - * will be notified to the Server with a minimum interval of 10 sec; - * this value is set at the Object Instance level. - * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 20 and new value is 35 due to step condition - * b.old value is 45 and new value is 50 due to gt condition - * c.old value is 50 and new value is 40 due to both gt and step conditions - * d.old value is 35 and new value is 20 due to step conditione) - * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 17 and new value is 24 due to lt condition - * b.old value is 75 and new value is 90 due to both gt and step conditions - * String uriQueries = "pmin=10&pmax=60"; - * AttributeSet attributes = AttributeSet.parse(uriQueries); - * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); - * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); - * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); - * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); - * Attribute [] attrs = {gt, st}; - */ - Attribute pmin = new Attribute(MINIMUM_PERIOD, Integer.toUnsignedLong(Integer.parseInt("1"))); - Attribute[] attrs = {pmin}; - AttributeSet attrSet = new AttributeSet(attrs); - if (resultIds.isResource()) { - request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), attrSet); - } else if (resultIds.isObjectInstance()) { - request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), attrSet); - } else if (resultIds.getObjectId() >= 0) { - request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet); - } + request = createWriteAttributeRequest(target, params); break; - default: } if (request != null) { - this.sendRequest(registration, lwM2MClient, request, timeoutInMs); - } - else { + try { + this.sendRequest(registration, lwM2MClient, request, timeoutInMs); + } catch (ClientSleepingException e) { + DownlinkRequest finalRequest = request; + long finalTimeoutInMs = timeoutInMs; + lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs)); + } catch (Exception e) { + log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper, e); + } + } else { log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); } } @@ -238,17 +204,21 @@ public class LwM2mTransportRequest { 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)); + lwM2MClient.initValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } - if (isSuccess(((Response) response.getCoapResponse()).getCode())) { + if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) { this.handleResponse(registration, request.getPath().toString(), response, request); if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) { + LwM2mNode node = ((WriteRequest) request).getNode(); + Object value = this.converter.convertValue(((LwM2mSingleResource) node).getValue(), + ((LwM2mSingleResource) node).getType(), ResourceModel.Type.STRING, request.getPath()); String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", - LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString(), - ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue().toString()); + LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), + response.getCode().getName(), request.getPath().toString(), value); serviceImpl.sendLogsToThingsboard(msg, registration); - log.info("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString(), - ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue()); + log.debug("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), + ((Response) response.getCoapResponse()).getCode(), response.getCode(), + request.getPath().toString(), value); } } else { String msg = String.format("%s: sendRequest: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s SendRequest to Client", LOG_LW2M_ERROR, @@ -258,7 +228,7 @@ public class LwM2mTransportRequest { } }, e -> { if (!lwM2MClient.isInit()) { - lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); + lwM2MClient.initValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } String msg = String.format("%s: sendRequest: Resource path - %s msg error - %s SendRequest to Client", LOG_LW2M_ERROR, request.getPath().toString(), e.toString()); @@ -283,7 +253,7 @@ public class LwM2mTransportRequest { case FLOAT: // Double return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString())); case TIME: // Date - Date date = new Date(Long.decode(value.toString())); + Date date = new Date(Long.decode(value.toString())); return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date); case OPAQUE: // byte[] value, base64 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())); @@ -317,7 +287,7 @@ public class LwM2mTransportRequest { * @param response - */ private void sendResponse(Registration registration, String path, LwM2mResponse response, DownlinkRequest request) { - String pathIdVer = convertToIdVerFromObjectId(path, registration); + String pathIdVer = convertPathFromObjectIdToIdVer(path, registration); if (response instanceof ReadResponse) { serviceImpl.onObservationResponse(registration, pathIdVer, (ReadResponse) response); } else if (response instanceof CancelObservationResponse) { 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 982e7cd74c..0ae4ef57aa 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 @@ -52,6 +52,10 @@ public interface LwM2mTransportService extends TbTransportService { void onResourceDelete(Optional resourceDeleteMsgOpt); + void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest); + + void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse); + 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 773fe2b729..30a3306146 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 @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.Sets; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -43,7 +44,6 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; @@ -53,12 +53,12 @@ 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.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PostConstruct; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; @@ -75,24 +75,22 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.transport.util.JsonUtils.getJsonObject; +import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.CLIENT_NOT_AUTHORIZED; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_REQUEST; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_READ; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LWM2M_STRATEGY_2; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.SERVICE_CHANNEL; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getAckCallback; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectIdFromKey; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectVerFromKey; @Slf4j @@ -147,9 +145,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * Next -> Create new LwM2MClient for current session -> setModelClient... * * @param registration - Registration LwM2M Client - * @param previousObsersations - may be null + * @param previousObservations - may be null */ - public void onRegistered(Registration registration, Collection previousObsersations) { + public void onRegistered(Registration registration, Collection previousObservations) { executorRegistered.submit(() -> { try { log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); @@ -161,6 +159,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), null); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); this.initLwM2mFromClientValue(registration, lwM2MClient); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration); } else { @@ -189,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo); if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) { initLwM2mClient(lwM2MClient, sessionInfo); + } else { + if (registration.getBindingMode().useQueueMode()) { + LwM2mQueuedRequest request; + while ((request = lwM2MClient.getQueuedRequests().poll()) != null) { + request.send(); + } + } } log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); @@ -240,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } } + @Override public void onSleepingDev(Registration registration) { log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint()); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration); + //TODO: associate endpointId with device information. } @@ -297,33 +306,32 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @Override public void onAttributeUpdate(AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo) { if (msg.getSharedUpdatedCount() > 0) { - JsonElement el = JsonConverter.toJson(msg); - el.getAsJsonObject().entrySet().forEach(de -> { - String pathIdVer = this.getPathAttributeUpdate(sessionInfo, de.getKey()); - String value = de.getValue().getAsString(); + msg.getSharedUpdatedList().forEach(tsKvProto -> { + String pathName = tsKvProto.getKv().getKey(); + String pathIdVer = this.validatePathIntoProfile(sessionInfo, pathName); + Object valueNew = this.lwM2mTransportContextServer.getValueFromKvProto(tsKvProto.getKv()); 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))) { + if (pathIdVer != null) { 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()); + this.updateResourcesValueToClient(lwM2MClient, this.getResourceValueFormatKv(lwM2MClient, pathIdVer), valueNew, pathIdVer); } else { - log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", pathIdVer, value); + log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", pathIdVer, valueNew); String logMsg = String.format("%s: attributeUpdate: Resource path - %s value - %s is not Writable and cannot be updated", - LOG_LW2M_ERROR, pathIdVer, value); + LOG_LW2M_ERROR, pathIdVer, valueNew); this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } } else { - log.error("Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value); + log.error("Resource name name - [{}] value - [{}] is not present as attribute/telemetry in profile and cannot be updated", pathName, valueNew); String logMsg = String.format("%s: attributeUpdate: attribute name - %s value - %s is not present as attribute in profile and cannot be updated", - LOG_LW2M_ERROR, de.getKey(), value); + LOG_LW2M_ERROR, pathName, valueNew); this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } }); } else if (msg.getSharedDeletedCount() > 0) { log.info("[{}] delete [{}] onAttributeUpdate", msg.getSharedDeletedList(), sessionInfo); } + } /** @@ -356,17 +364,15 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * * @param resourceUpdateMsgOpt - */ @Override - public void onResourceUpdate (Optional resourceUpdateMsgOpt) { + 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 @@ -375,6 +381,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.deleteResources(pathIdVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); } + public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest) { + log.info("[{}] toDeviceRpcRequest", toDeviceRequest); + } + + public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { + log.info("[{}] toServerRpcResponse", toServerResponse); + } + /** * Trigger Server path = "/1/0/8" *

@@ -418,6 +432,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ protected void onAwakeDev(Registration registration) { log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration); //TODO: associate endpointId with device information. } @@ -443,32 +458,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * @param msg - text msg + * @param logMsg - text msg * @param registration - Id of Registration LwM2M Client */ - public void sendLogsToThingsboard(String msg, Registration registration) { - if (msg != null) { - JsonObject telemetries = new JsonObject(); - telemetries.addProperty(LOG_LW2M_TELEMETRY, msg); - this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); - } - } - - - /** - * // !!! Ok - * Prepare send to Thigsboard callback - Attribute or Telemetry - * - * @param msg - JsonArray: [{name: value}] - * @param topicName - Api Attribute or Telemetry - * @param registration - Id of Registration LwM2M Client - */ - public void updateParametersOnThingsboard(JsonElement msg, String topicName, Registration registration) { + public void sendLogsToThingsboard(String logMsg, Registration registration) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); - if (sessionInfo != null) { - lwM2mTransportContextServer.sendParametersOnThingsboard(msg, topicName, sessionInfo); - } else { - log.error("Client: [{}] updateParametersOnThingsboard [{}] sessionInfo ", registration, null); + if (logMsg != null && sessionInfo != null) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardTelemetry(this.lwM2mTransportContextServer.getKvLogyToThingsboard(logMsg), sessionInfo); } } @@ -486,15 +482,20 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private void initLwM2mFromClientValue(Registration registration, LwM2mClient lwM2MClient) { LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - Set clientObjects = this.getAllOjectsInClient(registration); - if (clientObjects != null && LWM2M_STRATEGY_2 == LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { - // #2 - lwM2MClient.getPendingRequests().addAll(clientObjects); - clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), - null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + if (clientObjects != null && clientObjects.size() > 0) { + if (LWM2M_STRATEGY_2 == LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { + // #2 + lwM2MClient.getPendingRequests().addAll(clientObjects); + clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), + null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + } + // #1 + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_READ, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_OBSERVE, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, PUT_TYPE_OPER_WRITE_ATTRIBUTES, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_DISCOVER, clientObjects); } - // #1 - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_OBSERVE); } /** @@ -553,17 +554,20 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param registration - Registration LwM2M Client */ private void updateAttrTelemetry(Registration registration, Set paths) { - JsonObject attributes = new JsonObject(); - JsonObject telemetries = new JsonObject(); try { - this.getParametersFromProfile(attributes, telemetries, registration, paths); + ResultsAddKeyValueProto results = getParametersFromProfile(registration, paths); + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (results != null && sessionInfo != null) { + if (results.getResultAttributes().size() > 0) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo); + } + if (results.getResultTelemetries().size() > 0) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo); + } + } } catch (Exception e) { log.error("UpdateAttrTelemetry", e); } - if (attributes.getAsJsonObject().entrySet().size() > 0) - this.updateParametersOnThingsboard(attributes, DEVICE_ATTRIBUTES_TOPIC, registration); - if (telemetries.getAsJsonObject().entrySet().size() > 0) - this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); } /** @@ -601,41 +605,61 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * Start observe/read: Attr/Telemetry - * #1 - Analyze: - * #1.1 path in resource profile == client resource + * #1 - Analyze: path in resource profile == client resource * * @param registration - */ - private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, String typeOper) { + private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, + String typeOper, Set clientObjects) { LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - Set clientInstances = this.getAllInstancesInClient(registration); - Set result; + Set result = null; + ConcurrentHashMap params = null; if (GET_TYPE_OPER_READ.equals(typeOper)) { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() { - }); - result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() { - })); - } else { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() { - }); + result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), + new TypeReference<>() { + }); + result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), + new TypeReference<>() { + })); + } else if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), + new TypeReference<>() { + }); + } else if (GET_TYPE_OPER_DISCOVER.equals(typeOper)) { + result = this.getPathForWriteAttributes(lwM2MClientProfile.getPostAttributeLwm2mProfile()).keySet(); + ; + } else if (PUT_TYPE_OPER_WRITE_ATTRIBUTES.equals(typeOper)) { + params = this.getPathForWriteAttributes(lwM2MClientProfile.getPostAttributeLwm2mProfile()); + result = params.keySet(); } - Set pathSend = ConcurrentHashMap.newKeySet(); - result.forEach(target -> { - // #1.1 - String[] resPath = target.split("/"); - String instance = "/" + resPath[1] + "/" + resPath[2]; - if (clientInstances != null && clientInstances.size() > 0 && clientInstances.contains(instance)) { - pathSend.add(target); + if (!result.isEmpty()) { + // #1 + Set pathSend = result.stream().filter(target -> { + return target.split(LWM2M_SEPARATOR_PATH).length < 3 ? + clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1]) : + clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1] + "/" + target.split(LWM2M_SEPARATOR_PATH)[2]); + } + ) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + lwM2MClient.getPendingRequests().addAll(pathSend); + ConcurrentHashMap finalParams = params; + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), + null, finalParams != null ? finalParams.get(target) : null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + lwM2MClient.initValue(this, null); + } } - }); - lwM2MClient.getPendingRequests().addAll(pathSend); - pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), - null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); - if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { - lwM2MClient.initValue(this, null); } } + private ConcurrentHashMap getPathForWriteAttributes(JsonObject objectJson) { + ConcurrentHashMap pathAttributes = new Gson().fromJson(objectJson.toString(), + new TypeToken>() { + }.getType()); + return pathAttributes; + } + /** * Update parameters device in LwM2MClient * If new deviceProfile != old deviceProfile => update deviceProfile @@ -656,96 +680,109 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * @param registration - - * @return - all object in client - */ - private Set getAllOjectsInClient(Registration registration) { - Set clientObjects = ConcurrentHashMap.newKeySet(); - Arrays.stream(registration.getObjectLinks()).forEach(url -> { - LwM2mPath pathIds = new LwM2mPath(url.getUrl()); - if (pathIds.isObjectInstance()) { - clientObjects.add("/" + pathIds.getObjectId()); - } - }); - return (clientObjects.size() > 0) ? clientObjects : null; - } - - /** - * @param registration - - * @return all instances in client - */ - private Set getAllInstancesInClient(Registration registration) { - Set clientInstances = ConcurrentHashMap.newKeySet(); - Arrays.stream(registration.getObjectLinks()).forEach(url -> { - LwM2mPath pathIds = new LwM2mPath(url.getUrl()); - if (pathIds.isObjectInstance()) { - clientInstances.add(convertToIdVerFromObjectId(url.getUrl(), registration)); - } - }); - return (clientInstances.size() > 0) ? clientInstances : null; - } - - /** - * @param attributes - new JsonObject - * @param telemetry - new JsonObject + * // * @param attributes - new JsonObject + * // * @param telemetry - new JsonObject + * * @param registration - Registration LwM2M Client * @param path - */ - private void getParametersFromProfile(JsonObject attributes, JsonObject telemetry, Registration registration, Set path) { + private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set path) { if (path != null && path.size() > 0) { + ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - lwM2MClientProfile.getPostAttributeProfile().forEach(idVer -> { - if (path.contains(idVer.getAsString())) { - this.addParameters(idVer.getAsString(), attributes, registration); + List resultAttributes = new ArrayList<>(); + lwM2MClientProfile.getPostAttributeProfile().forEach(pathIdVer -> { + if (path.contains(pathIdVer.getAsString())) { + TransportProtos.KeyValueProto kvAttr = this.getKvToThingsboard(pathIdVer.getAsString(), registration); + if (kvAttr != null) { + resultAttributes.add(kvAttr); + } } }); - lwM2MClientProfile.getPostTelemetryProfile().forEach(idVer -> { - if (path.contains(idVer.getAsString())) { - this.addParameters(idVer.getAsString(), telemetry, registration); + List resultTelemetries = new ArrayList<>(); + lwM2MClientProfile.getPostTelemetryProfile().forEach(pathIdVer -> { + if (path.contains(pathIdVer.getAsString())) { + TransportProtos.KeyValueProto kvAttr = this.getKvToThingsboard(pathIdVer.getAsString(), registration); + if (kvAttr != null) { + resultTelemetries.add(kvAttr); + } } }); + if (resultAttributes.size() > 0) { + results.setResultAttributes(resultAttributes); + } + if (resultTelemetries.size() > 0) { + results.setResultTelemetries(resultTelemetries); + } + return results; } + return null; } - /** - * @param parameters - JsonObject attributes/telemetry - * @param registration - Registration LwM2M Client - */ - private void addParameters(String path, JsonObject parameters, Registration registration) { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); +// public TransportProtos.KeyValueProto getKvToThingsboard(String pathIdVer, Registration registration) { +// ResultsResourceValue resultsResourceValue = getResultsResourceValue(pathIdVer, registration); +// return resultsResourceValue != null ? this.lwM2mTransportContextServer.getKvAttrTelemetryToThingsboard(resultsResourceValue) : null; +// } + + private TransportProtos.KeyValueProto getKvToThingsboard(String pathIdVer, Registration registration) { + LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2mClientWithReg(null, registration.getId()); JsonObject names = lwM2mClientContext.getProfiles().get(lwM2MClient.getProfileId()).getPostKeyNameProfile(); - String resName = names.get(path).getAsString(); - if (resName != null && !resName.isEmpty()) { - try { - String resValue = this.getResourceValueToString(lwM2MClient, path); - if (resValue != null) { - parameters.addProperty(resName, resValue); + if (names != null && names.has(pathIdVer)) { + String resourceName = names.get(pathIdVer).getAsString(); + if (resourceName != null && !resourceName.isEmpty()) { + try { + LwM2mResource resourceValue = lwM2MClient != null ? getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer) : null; + if (resourceValue != null) { + ResourceModel.Type currentType = resourceValue.getType(); + ResourceModel.Type expectedType = this.lwM2mTransportContextServer.getResourceModelTypeEqualsKvProtoValueType(currentType, pathIdVer); + Object valueKvProto = null; + if (resourceValue.isMultiInstances()) { + valueKvProto = new JsonObject(); + Object finalvalueKvProto = valueKvProto; + Gson gson = new GsonBuilder().create(); + resourceValue.getValues().forEach((k, v) -> { + Object val = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); + JsonElement element = gson.toJsonTree(val, val.getClass()); + ((JsonObject) finalvalueKvProto).add(String.valueOf(k), element); + }); + valueKvProto = gson.toJson(valueKvProto); + } else { + valueKvProto = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); + } + return valueKvProto != null ? this.lwM2mTransportContextServer.getKvAttrTelemetryToThingsboard(currentType, resourceName, valueKvProto, resourceValue.isMultiInstances()) : null; + } + } catch (Exception e) { + log.error("Failed to add parameters.", e); } - } catch (Exception e) { - log.error("Failed to add parameters.", e); } + } else { + log.error("Failed to add parameters. path: [{}], names: [{}]", pathIdVer, names); } + return null; } /** - * @param path - path resource - * @return - value of Resource or null + * @param pathIdVer - path resource + * @return - value of Resource into format KvProto or null */ - private String getResourceValueToString(LwM2mClient lwM2MClient, String path) { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(path)); - LwM2mResource resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, path); - return resourceValue == null ? null : - this.converter.convertValue(resourceValue.isMultiInstances() ? resourceValue.getValues() : resourceValue.getValue(), resourceValue.getType(), ResourceModel.Type.STRING, pathIds).toString(); + private Object getResourceValueFormatKv(LwM2mClient lwM2MClient, String pathIdVer) { + LwM2mResource resourceValue = this.getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer); + ResourceModel.Type currentType = resourceValue.getType(); + ResourceModel.Type expectedType = this.lwM2mTransportContextServer.getResourceModelTypeEqualsKvProtoValueType(currentType, pathIdVer); + return this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); } /** * @param lwM2MClient - - * @param path - + * @param path - * @return - return value of Resource by idPath */ - private LwM2mResource returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { + private LwM2mResource getResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { LwM2mResource resourceValue = null; - if (new LwM2mPath(convertToObjectIdFromIdVer(path)).isResource()) { + if (new LwM2mPath(convertPathFromIdVerToObjectId(path)).isResource()) { resourceValue = lwM2MClient.getResources().get(path).getLwM2mResource(); } return resourceValue; @@ -769,6 +806,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * #3.1 Attribute isChange (add&del) * #3.2 Telemetry isChange (add&del) * #3.3 KeyName isChange (add) + * #3.4 attributeLwm2m isChange (update WrightAttribute: add/update/del) * #4 update * #4.1 add If #3 isChange, then analyze and update Value in Transport form Client and send Value to thingsboard * #4.2 del @@ -779,6 +817,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * -- path Attr/Telemetry includes newObserve and does not include oldObserve: send Request observe to Client * #5.3 Observe.del * -- different between newObserve and oldObserve: send Request cancel observe to client + * #6 + * #6.1 - update WriteAttribute + * #6.2 - del WriteAttribute * * @param registrationIds - * @param deviceProfile - @@ -793,6 +834,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Set telemetrySetOld = this.convertJsonArrayToSet(telemetryOld); JsonArray observeOld = lwM2MClientProfileOld.getPostObserveProfile(); JsonObject keyNameOld = lwM2MClientProfileOld.getPostKeyNameProfile(); + JsonObject attributeLwm2mOld = lwM2MClientProfileOld.getPostAttributeLwm2mProfile(); LwM2mClientProfile lwM2MClientProfileNew = lwM2mClientContext.getProfiles().get(deviceProfile.getUuidId()); JsonArray attributeNew = lwM2MClientProfileNew.getPostAttributeProfile(); @@ -801,32 +843,41 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Set telemetrySetNew = this.convertJsonArrayToSet(telemetryNew); JsonArray observeNew = lwM2MClientProfileNew.getPostObserveProfile(); JsonObject keyNameNew = lwM2MClientProfileNew.getPostKeyNameProfile(); + JsonObject attributeLwm2mNew = lwM2MClientProfileNew.getPostAttributeLwm2mProfile(); // #3 ResultsAnalyzerParameters sendAttrToThingsboard = new ResultsAnalyzerParameters(); // #3.1 if (!attributeOld.equals(attributeNew)) { - ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, new TypeToken>() { - }.getType()), attributeSetNew); + ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, + new TypeToken>() { + }.getType()), attributeSetNew); sendAttrToThingsboard.getPathPostParametersAdd().addAll(postAttributeAnalyzer.getPathPostParametersAdd()); sendAttrToThingsboard.getPathPostParametersDel().addAll(postAttributeAnalyzer.getPathPostParametersDel()); } // #3.2 if (!telemetryOld.equals(telemetryNew)) { - ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, new TypeToken>() { - }.getType()), telemetrySetNew); + ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, + new TypeToken>() { + }.getType()), telemetrySetNew); sendAttrToThingsboard.getPathPostParametersAdd().addAll(postTelemetryAnalyzer.getPathPostParametersAdd()); sendAttrToThingsboard.getPathPostParametersDel().addAll(postTelemetryAnalyzer.getPathPostParametersDel()); } // #3.3 if (!keyNameOld.equals(keyNameNew)) { - ResultsAnalyzerParameters keyNameChange = this.getAnalyzerKeyName(new Gson().fromJson(keyNameOld.toString(), new TypeToken>() { + ResultsAnalyzerParameters keyNameChange = this.getAnalyzerKeyName(new Gson().fromJson(keyNameOld.toString(), + new TypeToken>() { }.getType()), new Gson().fromJson(keyNameNew.toString(), new TypeToken>() { }.getType())); sendAttrToThingsboard.getPathPostParametersAdd().addAll(keyNameChange.getPathPostParametersAdd()); } + // #3.4, #6 + if (!attributeLwm2mOld.equals(attributeLwm2mNew)) { + this.getAnalyzerAttributeLwm2m(registrationIds, attributeLwm2mOld, attributeLwm2mNew); + } + // #4.1 add if (sendAttrToThingsboard.getPathPostParametersAdd().size() > 0) { // update value in Resources @@ -860,10 +911,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { // send Request observe to Client registrationIds.forEach(registrationId -> { Registration registration = lwM2mClientContext.getRegistration(registrationId); - this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); + if (postObserveAnalyzer.getPathPostParametersAdd().size() > 0) { + this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); + } // 5.3 del // send Request cancel observe to Client - this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); + if (postObserveAnalyzer.getPathPostParametersDel().size() > 0) { + this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); + } }); } } @@ -910,7 +965,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private void readResourceValueObserve(Registration registration, Set targets, String typeOper) { targets.forEach(target -> { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(target)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(target)); if (pathIds.isResource()) { if (GET_TYPE_OPER_READ.equals(typeOper)) { lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, @@ -923,7 +978,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { }); } - private ResultsAnalyzerParameters getAnalyzerKeyName(ConcurrentMap keyNameOld, ConcurrentMap keyNameNew) { + private ResultsAnalyzerParameters getAnalyzerKeyName(ConcurrentHashMap keyNameOld, ConcurrentHashMap keyNameNew) { ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); Set paths = keyNameNew.entrySet() .stream() @@ -933,22 +988,91 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { return analyzerParameters; } + /** + * #3.4, #6 + * #6 + * #6.1 - send update WriteAttribute + * #6.2 - send empty WriteAttribute + * + * @param attributeLwm2mOld - + * @param attributeLwm2mNew - + * @return + */ + private void getAnalyzerAttributeLwm2m(Set registrationIds, JsonObject attributeLwm2mOld, JsonObject attributeLwm2mNew) { + ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); + ConcurrentHashMap lwm2mAttributesOld = new Gson().fromJson(attributeLwm2mOld.toString(), + new TypeToken>() { + }.getType()); + ConcurrentHashMap lwm2mAttributesNew = new Gson().fromJson(attributeLwm2mNew.toString(), + new TypeToken>() { + }.getType()); + Set pathOld = lwm2mAttributesOld.keySet(); + Set pathNew = lwm2mAttributesNew.keySet(); + analyzerParameters.setPathPostParametersAdd(pathNew + .stream().filter(p -> !pathOld.contains(p)).collect(Collectors.toSet())); + analyzerParameters.setPathPostParametersDel(pathOld + .stream().filter(p -> !pathNew.contains(p)).collect(Collectors.toSet())); + Set pathCommon = pathNew + .stream().filter(p -> pathOld.contains(p)).collect(Collectors.toSet()); + Set pathCommonChange = pathCommon + .stream().filter(p -> !lwm2mAttributesOld.get(p).equals(lwm2mAttributesNew.get(p))).collect(Collectors.toSet()); + analyzerParameters.getPathPostParametersAdd().addAll(pathCommonChange); + // #6 + // #6.2 + if (analyzerParameters.getPathPostParametersAdd().size() > 0) { + registrationIds.forEach(registrationId -> { + Registration registration = this.lwM2mClientContext.getRegistration(registrationId); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + Set pathSend = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + ConcurrentHashMap finalParams = lwm2mAttributesNew; + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, PUT_TYPE_OPER_WRITE_ATTRIBUTES, ContentFormat.TLV.getName(), + null, finalParams.get(target), this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + } + }); + } + // #6.2 + if (analyzerParameters.getPathPostParametersDel().size() > 0) { + registrationIds.forEach(registrationId -> { + Registration registration = this.lwM2mClientContext.getRegistration(registrationId); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + Set pathSend = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + pathSend.forEach(target -> { + Map params = (Map) lwm2mAttributesOld.get(target); + params.clear(); + params.put(OBJECT_VERSION, ""); + lwM2mTransportRequest.sendAllRequest(registration, target, PUT_TYPE_OPER_WRITE_ATTRIBUTES, ContentFormat.TLV.getName(), + null, params, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + }); + } + }); + } + + } + private void cancelObserveIsValue(Registration registration, Set paramAnallyzer) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); paramAnallyzer.forEach(p -> { - if (this.returnResourceValueFromLwM2MClient(lwM2MClient, p) != null) { - this.setCancelObservationRecourse(registration, convertToObjectIdFromIdVer(p)); + if (this.getResourceValueFromLwM2MClient(lwM2MClient, p) != null) { + this.setCancelObservationRecourse(registration, convertPathFromIdVerToObjectId(p)); } } ); } - private void putDelayedUpdateResourcesClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { + private void updateResourcesValueToClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { if (valueNew != null && (valueOld == null || !valueNew.toString().equals(valueOld.toString()))) { lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE, ContentFormat.TLV.getName(), null, valueNew, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); } else { - log.error("05 delayError"); + log.error("Failed update resource [{}] [{}]", path, valueNew); + String logMsg = String.format("%s: Failed update resource path - %s value - %s. Value is not changed or bad", + LOG_LW2M_ERROR, path, valueNew); + this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + log.info("Failed update resource [{}] [{}]", path, valueNew); } } @@ -968,9 +1092,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param name - * @return path if path isPresent in postProfile */ - private String getPathAttributeUpdate(TransportProtos.SessionInfoProto sessionInfo, String name) { - String profilePath = this.getPathAttributeUpdateProfile(sessionInfo, name); - return !profilePath.isEmpty() ? profilePath : null; + private String validatePathIntoProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { + String pathIdVer = this.getPresentPathIntoProfile(sessionInfo, name); + return !pathIdVer.isEmpty() ? pathIdVer : null; } /** @@ -980,7 +1104,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param name - * @return - */ - private String getPathAttributeUpdateProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { + private String getPresentPathIntoProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { LwM2mClientProfile profile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); LwM2mClient lwM2mClient = lwM2mClientContext.getLwM2MClient(sessionInfo); return profile.getPostKeyNameProfile().getAsJsonObject().entrySet().stream() @@ -991,40 +1115,49 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * Update resource value on client: if there is a difference in values between the current resource values and the shared attribute values * #1 Get path resource by result attributesResponse - * #1.1 If two names have equal path => last time attribute - * #2.1 if there is a difference in values between the current resource values and the shared attribute values - * => send to client Request Update of value (new value from shared attribute) - * and LwM2MClient.delayedRequests.add(path) - * #2.1 if there is not a difference in values between the current resource values and the shared attribute values * * @param attributesResponse - * @param sessionInfo - */ public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg attributesResponse, TransportProtos.SessionInfoProto sessionInfo) { try { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); - attributesResponse.getSharedAttributeListList().forEach(attr -> { - String path = this.getPathAttributeUpdate(sessionInfo, attr.getKv().getKey()); - if (path != null) { - // #1.1 - if (lwM2MClient.getDelayedRequests().containsKey(path) && attr.getTs() > lwM2MClient.getDelayedRequests().get(path).getTs()) { - lwM2MClient.getDelayedRequests().put(path, attr); - } else { - lwM2MClient.getDelayedRequests().put(path, attr); - } - } - }); - // #2.1 - lwM2MClient.getDelayedRequests().forEach((k, v) -> { - ArrayList listV = new ArrayList<>(); - listV.add(v.getKv()); - this.putDelayedUpdateResourcesClient(lwM2MClient, this.getResourceValueToString(lwM2MClient, k), getJsonObject(listV).get(v.getKv().getKey()), k); - }); + List tsKvProtos = attributesResponse.getSharedAttributeListList(); + this.updateAttriuteFromThingsboard(tsKvProtos, sessionInfo); } catch (Exception e) { log.error(String.valueOf(e)); } } + /** + * #1.1 If two names have equal path => last time attribute + * #2.1 if there is a difference in values between the current resource values and the shared attribute values + * => send to client Request Update of value (new value from shared attribute) + * and LwM2MClient.delayedRequests.add(path) + * #2.1 if there is not a difference in values between the current resource values and the shared attribute values + * + * @param tsKvProtos + * @param sessionInfo + */ + public void updateAttriuteFromThingsboard(List tsKvProtos, TransportProtos.SessionInfoProto sessionInfo) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); + tsKvProtos.forEach(tsKvProto -> { + String pathIdVer = this.validatePathIntoProfile(sessionInfo, tsKvProto.getKv().getKey()); + if (pathIdVer != null) { + // #1.1 + if (lwM2MClient.getDelayedRequests().containsKey(pathIdVer) && tsKvProto.getTs() > lwM2MClient.getDelayedRequests().get(pathIdVer).getTs()) { + lwM2MClient.getDelayedRequests().put(pathIdVer, tsKvProto); + } else if (!lwM2MClient.getDelayedRequests().containsKey(pathIdVer)) { + lwM2MClient.getDelayedRequests().put(pathIdVer, tsKvProto); + } + } + }); + // #2.1 + lwM2MClient.getDelayedRequests().forEach((pathIdVer, tsKvProto) -> { + this.updateResourcesValueToClient(lwM2MClient, this.getResourceValueFormatKv(lwM2MClient, pathIdVer), + this.lwM2mTransportContextServer.getValueFromKvProto(tsKvProto.getKv()), pathIdVer); + }); + } + /** * @param lwM2MClient - * @return SessionInfoProto - @@ -1141,11 +1274,11 @@ 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 ? + private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathIdVer, boolean isWritableNotOptional) { + ResourceModel resourceModel = lwM2mClient.getResourceModel(pathIdVer); + Integer objectId = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer)).getObjectId(); + String objectVer = validateObjectVerFromKey(pathIdVer); + return resourceModel != null && (isWritableNotOptional ? objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)) && resourceModel.operations.isWritable() : objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId))); } 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 bdb783e3ff..5e736d8e0f 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 @@ -25,18 +25,21 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; 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; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; @Slf4j @Data @@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable { private final Map resources; private final Map delayedRequests; private final List pendingRequests; + private final Queue queuedRequests; private boolean init; public Object clone() throws CloneNotSupportedException { @@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable { this.profileId = profileId; this.sessionId = sessionId; this.init = false; + this.queuedRequests = new ConcurrentLinkedQueue<>(); } public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { @@ -78,7 +83,7 @@ public class LwM2mClient implements Cloneable { this.resources.get(pathRez).setLwM2mResource(rez); return true; } else { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.put(pathRez, new ResourceValue(rez, resourceModel)); @@ -103,9 +108,9 @@ public class LwM2mClient implements Cloneable { * @param modelProvider - */ public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { - Set key = getKeysEqualsIdVer(pathIdVer); + Set key = getKeysEqualsIdVer(pathIdVer); key.forEach(pathRez -> { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez.toString())); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.get(pathRez).setResourceModel(resourceModel); @@ -122,17 +127,17 @@ public class LwM2mClient implements Cloneable { * @param modelProvider - */ public void updateResourceModel(String idVer, LwM2mModelProvider modelProvider) { - Set key = getKeysEqualsIdVer(idVer); - key.forEach(k -> this.saveResourceModel(k.toString(), modelProvider)); + Set key = getKeysEqualsIdVer(idVer); + key.forEach(k -> this.saveResourceModel(k, modelProvider)); } private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); this.resources.get(pathRez).setResourceModel(resourceModel); } - private Set getKeysEqualsIdVer(String idVer) { + private Set getKeysEqualsIdVer(String idVer) { return this.resources.keySet() .stream() .filter(e -> idVer.equals(e.split(LWM2M_SEPARATOR_PATH)[1])) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index 2aea3fdc3e..358f760fef 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Map; +import java.util.Set; import java.util.UUID; public interface LwM2mClientContext { @@ -51,4 +52,6 @@ public interface LwM2mClientContext { Map setProfiles(Map profiles); boolean addUpdateProfileParameters(DeviceProfile deviceProfile); + + Set getSupportedIdVerInClient(Registration registration); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 926d12516b..cca729f5ff 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.client; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.springframework.stereotype.Service; @@ -27,11 +28,14 @@ import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; import org.thingsboard.server.transport.lwm2m.utils.TypeServer; +import java.util.Arrays; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; @Service @TbLwM2mTransportComponent @@ -90,7 +94,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public LwM2mClient updateInSessionsLwM2MClient(Registration registration) { if (this.lwM2mClients.get(registration.getEndpoint()) == null) { - addLwM2mClientToSession(registration.getEndpoint()); + this.addLwM2mClientToSession(registration.getEndpoint()); } LwM2mClient lwM2MClient = lwM2mClients.get(registration.getEndpoint()); lwM2MClient.setRegistration(registration); @@ -169,4 +173,21 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } return false; } + + /** + * if isVer - ok or default ver=DEFAULT_LWM2M_VERSION + * @param registration - + * @return - all objectIdVer in client + */ + @Override + public Set getSupportedIdVerInClient(Registration registration) { + Set clientObjects = ConcurrentHashMap.newKeySet(); + Arrays.stream(registration.getObjectLinks()).forEach(url -> { + LwM2mPath pathIds = new LwM2mPath(url.getUrl()); + if (!pathIds.isRoot()) { + clientObjects.add(convertPathFromObjectIdToIdVer(url.getUrl(), registration)); + } + }); + return (clientObjects.size() > 0) ? clientObjects : null; + } } 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 1c4042bd1a..19af453f3c 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 @@ -56,6 +56,13 @@ public class LwM2mClientProfile { */ private JsonArray postObserveProfile; + /** + * "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"}, + * "/3_1.0/0": {"gt": 17}, + * "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}} + */ + private JsonObject postAttributeLwm2mProfile; + public LwM2mClientProfile clone() { LwM2mClientProfile lwM2mClientProfile = new LwM2mClientProfile(); lwM2mClientProfile.postClientLwM2mSettings = this.deepCopy(this.postClientLwM2mSettings, JsonObject.class); @@ -63,6 +70,7 @@ public class LwM2mClientProfile { lwM2mClientProfile.postAttributeProfile = this.deepCopy(this.postAttributeProfile, JsonArray.class); lwM2mClientProfile.postTelemetryProfile = this.deepCopy(this.postTelemetryProfile, JsonArray.class); lwM2mClientProfile.postObserveProfile = this.deepCopy(this.postObserveProfile, JsonArray.class); + lwM2mClientProfile.postAttributeLwm2mProfile = this.deepCopy(this.postAttributeLwm2mProfile, JsonObject.class); return lwM2mClientProfile; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.java new file mode 100644 index 0000000000..5c7edbe328 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.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.transport.lwm2m.server.client; + +import lombok.Data; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ResultsAddKeyValueProto { + List resultAttributes; + List resultTelemetries; + + public ResultsAddKeyValueProto() { + this.resultAttributes = new ArrayList<>(); + this.resultTelemetries = new ArrayList<>(); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.java new file mode 100644 index 0000000000..36cd8943fb --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.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.transport.lwm2m.server.client; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.DataType; + +@Data +public class ResultsResourceValue { + DataType dataType; + Object value; + String resourceName; + + public ResultsResourceValue (DataType dataType, Object value, String resourceName) { + this.dataType = dataType; + this.value = value; + this.resourceName = resourceName; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 0faa20952e..dce34c9cf6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -18,14 +18,12 @@ package org.thingsboard.server.transport.lwm2m.utils; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.ObjectLink; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.core.util.StringUtils; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; import java.math.BigInteger; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -111,15 +109,16 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { case INTEGER: log.debug("Trying to convert long value {} to date", value); /** let's assume we received the millisecond since 1970/1/1 */ - return new Date((Long) value); + return new Date(((Number) value).longValue() * 1000L); case STRING: log.debug("Trying to convert string value {} to date", value); /** let's assume we received an ISO 8601 format date */ try { - DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); - XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); - return cal.toGregorianCalendar().getTime(); - } catch (DatatypeConfigurationException | IllegalArgumentException e) { + return new Date(Long.decode(value.toString())); +// DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); +// XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); +// return cal.toGregorianCalendar().getTime(); + } catch (IllegalArgumentException e) { log.debug("Unable to convert string to date", e); throw new CodecException("Unable to convert string (%s) to date for resource %s", value, resourcePath); @@ -147,6 +146,8 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { return formatter.format(new Date(timeValue)); case OPAQUE: return Hex.encodeHexString((byte[])value); + case OBJLNK: + return ObjectLink.decodeFromString((String) value); default: break; } @@ -164,10 +165,14 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { } } break; + case OBJLNK: + if (currentType == Type.STRING) { + return ObjectLink.fromPath(value.toString()); + } default: } throw new CodecException("Invalid value type for resource %s, expected %s, got %s", resourcePath, expectedType, currentType); } -} + } 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 c948e755c2..30178bd44f 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.mqtt.adaptors; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; @@ -64,9 +65,9 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor attributesDynamicMessage = getDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); + Descriptors.Descriptor attributesDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(bytes, attributesDynamicMessage))); + return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -86,8 +87,8 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { byte[] bytes = toBytes(inbound.payload()); String topicName = inbound.variableHeader().topicName(); - int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); try { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); } catch (InvalidProtocolBufferException e) { log.warn("Failed to decode get attributes request", e); @@ -97,10 +98,15 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { @Override public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + String topicName = mqttMsg.variableHeader().topicName(); byte[] bytes = toBytes(mqttMsg.payload()); + Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); try { - return TransportProtos.ToDeviceRpcResponseMsg.parseFrom(bytes); - } catch (RuntimeException | InvalidProtocolBufferException e) { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); + JsonElement response = new JsonParser().parse(dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); + } catch (Exception e) { log.warn("Failed to decode Rpc response", e); throw new AdaptorException(e); } @@ -145,8 +151,10 @@ 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(), ProtoConverter.convertToRpcRequest(rpcRequest))); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + DynamicMessage.Builder rpcRequestDynamicMessageBuilder = deviceSessionCtx.getRpcRequestDynamicMessageBuilder(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder))); } @Override diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index efc5c057ed..3804d96cf6 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt.session; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; import lombok.Setter; @@ -60,6 +61,8 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor; private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; + private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; + private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; @Getter @Setter @@ -102,6 +105,14 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { return attributesDynamicMessageDescriptor; } + public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor() { + return rpcResponseDynamicMessageDescriptor; + } + + public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder() { + return rpcRequestDynamicMessageBuilder; + } + @Override public void setDeviceProfile(DeviceProfile deviceProfile) { super.setDeviceProfile(deviceProfile); @@ -136,5 +147,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { ProtoTransportPayloadConfiguration protoTransportPayloadConfig = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; telemetryDynamicMessageDescriptor = protoTransportPayloadConfig.getTelemetryDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceTelemetryProtoSchema()); attributesDynamicMessageDescriptor = protoTransportPayloadConfig.getAttributesDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceAttributesProtoSchema()); + rpcResponseDynamicMessageDescriptor = protoTransportPayloadConfig.getRpcResponseDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceRpcResponseProtoSchema()); + rpcRequestDynamicMessageBuilder = protoTransportPayloadConfig.getRpcRequestDynamicMessageBuilder(protoTransportPayloadConfig.getDeviceRpcRequestProtoSchema()); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java index 42c99bc428..d0d471a784 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java @@ -31,4 +31,8 @@ public class AdaptorException extends Exception { super(cause); } + public AdaptorException(String message, Exception cause) { + super(message, cause); + } + } 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 ed440ebcfe..620728cda4 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,10 +15,13 @@ */ package org.thingsboard.server.common.transport.adaptor; +import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -34,6 +37,7 @@ import java.util.List; @Slf4j public class ProtoConverter { + public static final Gson GSON = new Gson(); public static final JsonParser JSON_PARSER = new JsonParser(); public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { @@ -170,26 +174,20 @@ 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) { + public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + rpcRequestDynamicMessageBuilder.clear(); + JsonObject rpcRequestJson = new JsonObject(); + rpcRequestJson.addProperty("method", toDeviceRpcRequestMsg.getMethodName()); + rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId()); 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; - } + try { + JsonElement paramsElement = JSON_PARSER.parse(params); + rpcRequestJson.add("params", paramsElement); + JsonFormat.parser().ignoringUnknownFields().merge(GSON.toJson(rpcRequestJson), rpcRequestDynamicMessageBuilder); + DynamicMessage dynamicRpcRequest = rpcRequestDynamicMessageBuilder.build(); + return dynamicRpcRequest.toByteArray(); + } catch (Exception e) { + throw new AdaptorException("Failed to convert ToDeviceRpcRequestMsg to Dynamic Rpc request message due to: ", e); } } } 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 5603721474..cb2af9eb3f 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 @@ -207,6 +207,8 @@ public class LwM2MTransportConfigServer { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("bin$", "")); } else if (BASE_DIR_PATH.endsWith("conf")) { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("conf$", "")); + } else if (BASE_DIR_PATH.endsWith("application")) { + FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.substring(0, BASE_DIR_PATH.length() - "application".length())); } else { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH); } diff --git a/dao/pom.xml b/dao/pom.xml index 9ba3033970..cc25e1a13c 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -227,10 +227,6 @@ org.elasticsearch.client rest - - org.eclipse.leshan - leshan-core - diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index b48722cf06..5c7c1a5029 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.device; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.squareup.wire.Syntax; import com.squareup.wire.schema.Field; import com.squareup.wire.schema.Location; @@ -87,6 +89,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D private static final Location LOCATION = new Location("", "", -1, -1); private static final String ATTRIBUTES_PROTO_SCHEMA = "attributes proto schema"; private static final String TELEMETRY_PROTO_SCHEMA = "telemetry proto schema"; + private static final String RPC_REQUEST_PROTO_SCHEMA = "rpc request proto schema"; + private static final String RPC_RESPONSE_PROTO_SCHEMA = "rpc response proto schema"; private static String invalidSchemaProvidedMessage(String schemaName) { return "[Transport Configuration] invalid " + schemaName + " provided!"; @@ -358,9 +362,10 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; if (mqttTransportConfiguration.getTransportPayloadTypeConfiguration() instanceof ProtoTransportPayloadConfiguration) { - ProtoTransportPayloadConfiguration protoTransportPayloadTypeConfiguration = + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - validateProtoSchemas(protoTransportPayloadTypeConfiguration); + validateProtoSchemas(protoTransportPayloadConfiguration); + validateRpcRequestDynamicMessageFields(protoTransportPayloadConfiguration); } } else if (transportConfiguration instanceof CoapDeviceProfileTransportConfiguration) { CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; @@ -371,6 +376,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration) { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; validateProtoSchemas(protoTransportPayloadConfiguration); + validateRpcRequestDynamicMessageFields(protoTransportPayloadConfiguration); } } } @@ -418,6 +424,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D try { validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceAttributesProtoSchema(), ATTRIBUTES_PROTO_SCHEMA); validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceTelemetryProtoSchema(), TELEMETRY_PROTO_SCHEMA); + validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceRpcRequestProtoSchema(), RPC_REQUEST_PROTO_SCHEMA); + validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceRpcResponseProtoSchema(), RPC_RESPONSE_PROTO_SCHEMA); } catch (Exception exception) { throw new DataValidationException(exception.getMessage()); } @@ -540,6 +548,48 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D }; + private void validateRpcRequestDynamicMessageFields(ProtoTransportPayloadConfiguration protoTransportPayloadTypeConfiguration) { + DynamicMessage.Builder rpcRequestDynamicMessageBuilder = protoTransportPayloadTypeConfiguration.getRpcRequestDynamicMessageBuilder(protoTransportPayloadTypeConfiguration.getDeviceRpcRequestProtoSchema()); + Descriptors.Descriptor rpcRequestDynamicMessageDescriptor = rpcRequestDynamicMessageBuilder.getDescriptorForType(); + if (rpcRequestDynamicMessageDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get rpcRequestDynamicMessageDescriptor!"); + } else { + if (CollectionUtils.isEmpty(rpcRequestDynamicMessageDescriptor.getFields()) || rpcRequestDynamicMessageDescriptor.getFields().size() != 3) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " " + rpcRequestDynamicMessageDescriptor.getName() + " message should always contains 3 fields: method, requestId and params!"); + } + Descriptors.FieldDescriptor methodFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("method"); + if (methodFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: method!"); + } else { + if (!Descriptors.FieldDescriptor.Type.STRING.equals(methodFieldDescriptor.getType())) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'method' has invalid data type. Only string type is supported!"); + } + if (methodFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'method' has invalid label!"); + } + } + Descriptors.FieldDescriptor requestIdFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("requestId"); + if (requestIdFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: requestId!"); + } else { + if (!Descriptors.FieldDescriptor.Type.INT32.equals(requestIdFieldDescriptor.getType())) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'requestId' has invalid data type. Only int32 type is supported!"); + } + if (requestIdFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'requestId' has invalid label!"); + } + } + Descriptors.FieldDescriptor paramsFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("params"); + if (paramsFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: params!"); + } else { + if (paramsFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + "Field 'params' has invalid label!"); + } + } + } + } + private PaginatedRemover tenantDeviceProfilesRemover = new PaginatedRemover() { 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 80fcf04430..ece9c20284 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 @@ -627,6 +627,11 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); } + @Override + public long countByTenantId(TenantId tenantId) { + return deviceDao.countByTenantId(tenantId); + } + private DataValidator deviceValidator = new DataValidator() { 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 index fe50a63907..8401a43d21 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -586,7 +586,7 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic @Override public Object activateInstance(String edgeLicenseSecret, String releaseDate) { - Map params = new HashMap(); + 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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index 4cc853b2b9..91bfa954fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -30,7 +31,6 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -111,6 +111,11 @@ public class BaseEventService implements EventService { return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit); } + @Override + public PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { + return eventDao.findEventByFilter(tenantId.getId(), entityId, eventFilter, pageLink); + } + @Override public void removeEvents(TenantId tenantId, EntityId entityId) { PageData eventPageData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index 0cb4fccfb3..ba4e86c95e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.event; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -88,6 +89,8 @@ public interface EventDao extends Dao { */ PageData findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + PageData findEventByFilter(UUID tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); + /** * Find latest events by tenantId, entityId and eventType. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 44b328fd9f..15ddb9a390 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -179,11 +179,28 @@ public class BaseRelationService implements RelationService { @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { - try { - deleteEntityRelationsAsync(tenantId, entityId).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + log.trace("Executing deleteEntityRelations [{}]", entityId); + validate(entityId); + final Cache cache = cacheManager.getCache(RELATIONS_CACHE); + List inboundRelations = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + inboundRelations.addAll(relationDao.findAllByTo(tenantId, entityId, typeGroup)); + } + + List outboundRelations = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + outboundRelations.addAll(relationDao.findAllByFrom(tenantId, entityId, typeGroup)); + } + + for (EntityRelation relation : inboundRelations){ + delete(tenantId, cache, relation, true); } + + for (EntityRelation relation : outboundRelations){ + delete(tenantId, cache, relation, false); + } + + relationDao.deleteOutboundRelations(tenantId, entityId); } @Override @@ -193,14 +210,14 @@ public class BaseRelationService implements RelationService { validate(entityId); List>> inboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList.add(relationDao.findAllByTo(tenantId, entityId, typeGroup)); + inboundRelationsList.add(relationDao.findAllByToAsync(tenantId, entityId, typeGroup)); } ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); List>> outboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - outboundRelationsList.add(relationDao.findAllByFrom(tenantId, entityId, typeGroup)); + outboundRelationsList.add(relationDao.findAllByFromAsync(tenantId, entityId, typeGroup)); } ListenableFuture>> outboundRelations = Futures.allAsList(outboundRelationsList); @@ -242,6 +259,15 @@ public class BaseRelationService implements RelationService { } } + boolean delete(TenantId tenantId, Cache cache, EntityRelation relation, boolean deleteFromDb) { + cacheEviction(relation, cache); + if (deleteFromDb) { + return relationDao.deleteRelation(tenantId, relation); + } else { + return false; + } + } + private void cacheEviction(EntityRelation relation, Cache cache) { List fromToTypeAndTypeGroup = new ArrayList<>(); fromToTypeAndTypeGroup.add(relation.getFrom()); @@ -283,7 +309,7 @@ public class BaseRelationService implements RelationService { validate(from); validateTypeGroup(typeGroup); try { - return relationDao.findAllByFrom(tenantId, from, typeGroup).get(); + return relationDao.findAllByFromAsync(tenantId, from, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -306,7 +332,7 @@ public class BaseRelationService implements RelationService { if (fromCache != null) { return Futures.immediateFuture(fromCache); } else { - ListenableFuture> relationsFuture = relationDao.findAllByFrom(tenantId, from, typeGroup); + ListenableFuture> relationsFuture = relationDao.findAllByFromAsync(tenantId, from, typeGroup); Futures.addCallback(relationsFuture, new FutureCallback>() { @Override @@ -327,7 +353,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup); validate(from); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByFrom(tenantId, from, typeGroup); + ListenableFuture> relations = relationDao.findAllByFromAsync(tenantId, from, typeGroup); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); @@ -365,7 +391,7 @@ public class BaseRelationService implements RelationService { validate(to); validateTypeGroup(typeGroup); try { - return relationDao.findAllByTo(tenantId, to, typeGroup).get(); + return relationDao.findAllByToAsync(tenantId, to, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -388,7 +414,7 @@ public class BaseRelationService implements RelationService { if (fromCache != null) { return Futures.immediateFuture(fromCache); } else { - ListenableFuture> relationsFuture = relationDao.findAllByTo(tenantId, to, typeGroup); + ListenableFuture> relationsFuture = relationDao.findAllByToAsync(tenantId, to, typeGroup); Futures.addCallback(relationsFuture, new FutureCallback>() { @Override @@ -409,7 +435,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByTo(tenantId, to, typeGroup); + ListenableFuture> relations = relationDao.findAllByToAsync(tenantId, to, typeGroup); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index a8c4d9bbc2..d60f176832 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -31,11 +31,15 @@ import java.util.List; */ public interface RelationDao { - ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); + ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); + + List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup); - ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); + ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); + + List findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java similarity index 55% rename from dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java rename to dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 0df2f19e80..97675d4b3e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -17,10 +17,6 @@ package org.thingsboard.server.dao.resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.eclipse.leshan.core.model.DDFFileParser; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; -import org.eclipse.leshan.core.model.InvalidDDFFileException; -import org.eclipse.leshan.core.model.ObjectModel; import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ResourceType; @@ -29,9 +25,6 @@ import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; 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.LwM2mResourceObserve; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -41,63 +34,30 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -public class BaseTbResourceService implements TbResourceService { +public class BaseResourceService implements ResourceService { public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; private final TbResourceDao resourceDao; private final TbResourceInfoDao resourceInfoDao; private final TenantDao tenantDao; - private final DDFFileParser ddfFileParser; - public BaseTbResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { + + public BaseResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { this.resourceDao = resourceDao; this.resourceInfoDao = resourceInfoDao; this.tenantDao = tenantDao; - this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); } @Override - public TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException { - log.trace("Executing saveResource [{}]", resource); - if (StringUtils.isEmpty(resource.getData())) { - throw new DataValidationException("Resource data should be specified!"); - } - if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { - List objectModels = - ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (!objectModels.isEmpty()) { - ObjectModel objectModel = objectModels.get(0); - - String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); - String name = objectModel.name; - resource.setResourceKey(resourceKey); - 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())); - } - } else { - resource.setResourceKey(resource.getFileName()); - } - + public TbResource saveResource(TbResource resource) { resourceValidator.validate(resource, TbResourceInfo::getTenantId); try { @@ -111,7 +71,6 @@ public class BaseTbResourceService implements TbResourceService { throw t; } } - } @Override @@ -156,31 +115,17 @@ public class BaseTbResourceService implements TbResourceService { } @Override - public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { - log.trace("Executing findByTenantId [{}]", tenantId); + public List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) { + log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - PageData resourcePageData = resourceDao.findResourcesByTenantIdAndResourceType( - tenantId, - ResourceType.LWM2M_MODEL, pageLink); - return resourcePageData.getData().stream() - .map(this::toLwM2mObject) - .sorted(getComparator(sortProperty, sortOrder)) - .collect(Collectors.toList()); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null); } @Override - public List findLwM2mObject(TenantId tenantId, String sortOrder, - String sortProperty, - String[] objectIds) { - log.trace("Executing findByTenantId [{}]", tenantId); + public PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) { + log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - List resources = resourceDao.findResourcesByTenantIdAndResourceType(tenantId, ResourceType.LWM2M_MODEL, - objectIds, - null); - return resources.stream() - .map(this::toLwM2mObject) - .sorted(getComparator(sortProperty, sortOrder)) - .collect(Collectors.toList()); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink); } @Override @@ -190,50 +135,6 @@ public class BaseTbResourceService implements TbResourceService { tenantResourcesRemover.removeEntities(tenantId, tenantId); } - private LwM2mObject toLwM2mObject(TbResource resource) { - try { - DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); - List objectModels = - ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (objectModels.size() == 0) { - return null; - } else { - ObjectModel obj = objectModels.get(0); - LwM2mObject lwM2mObject = new LwM2mObject(); - lwM2mObject.setId(obj.id); - lwM2mObject.setKeyId(resource.getResourceKey()); - lwM2mObject.setName(obj.name); - lwM2mObject.setMultiple(obj.multiple); - lwM2mObject.setMandatory(obj.mandatory); - LwM2mInstance instance = new LwM2mInstance(); - instance.setId(0); - List resources = new ArrayList<>(); - obj.resources.forEach((k, v) -> { - if (!v.operations.isExecutable()) { - LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); - resources.add(lwM2MResourceObserve); - } - }); - instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); - lwM2mObject.setInstances(new LwM2mInstance[]{instance}); - return lwM2mObject; - } - } catch (IOException | InvalidDDFFileException e) { - log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); - return null; - } - } - - private Comparator getComparator(String sortProperty, String sortOrder) { - Comparator comparator; - if ("name".equals(sortProperty)) { - comparator = Comparator.comparing(LwM2mObject::getName); - } else { - comparator = Comparator.comparingLong(LwM2mObject::getId); - } - return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; - } - private DataValidator resourceValidator = new DataValidator<>() { @Override @@ -259,9 +160,6 @@ public class BaseTbResourceService implements TbResourceService { throw new DataValidationException("Resource is referencing to non-existent tenant!"); } } - if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource) == null) { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); - } } }; 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 b85a257ed2..786d86d8dd 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 @@ -26,6 +26,7 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; @@ -96,23 +97,19 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private TbTenantProfileCache tenantProfileCache; @Override + @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); if (ruleChain.isRoot() && ruleChain.getId() == null) { - try { - createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (Exception e) { - log.warn("[{}] Failed to create tenant to root rule chain relation. from: [{}], to: [{}]", - savedRuleChain.getTenantId(), savedRuleChain.getTenantId(), savedRuleChain.getId(), e); - throw new RuntimeException(e); - } + createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); } return savedRuleChain; } @Override + @Transactional public boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId) { RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); if (!ruleChain.isRoot()) { @@ -145,11 +142,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public RuleChainMetaData saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { + @Transactional + public boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { - return null; + return false; } if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { @@ -184,14 +182,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChain.getId()); RuleNode savedNode = ruleNodeDao.save(tenantId, node); - try { - createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (Exception e) { - log.warn("[{}] Failed to create rule chain to rule node relation. from: [{}], to: [{}]", - ruleChainMetaData.getRuleChainId(), savedNode.getId()); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); int index = nodes.indexOf(node); nodes.set(index, savedNode); ruleNodeIndexMap.put(savedNode.getId(), index); @@ -213,12 +205,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC EntityId from = nodes.get(nodeConnection.getFromIndex()).getId(); EntityId to = nodes.get(nodeConnection.getToIndex()).getId(); String type = nodeConnection.getType(); - try { - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); - } catch (Exception e) { - log.warn("[{}] Failed to create rule node relation. from: [{}], to: [{}]", from, to); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); } } if (ruleChainMetaData.getRuleChainConnections() != null) { @@ -226,16 +213,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId(); EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); String type = nodeToRuleChainConnection.getType(); - try { - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); - } catch (Exception e) { - log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); } } - return loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); + return true; } private void validateCircles(List connectionInfos) { @@ -410,6 +392,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override + @Transactional public void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request."); RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); @@ -716,7 +699,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeDao.removeById(tenantId, entityId.getId()); } - private DataValidator ruleChainValidator = new DataValidator() { @Override 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 index e16aebbfea..b1dcf5b47a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java @@ -15,7 +15,6 @@ */ 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; @@ -25,6 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import java.util.Optional; @Slf4j public class NoXssValidator implements ConstraintValidator { @@ -34,17 +34,21 @@ public class NoXssValidator implements ConstraintValidator { @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()); - } + xssPolicy = Optional.ofNullable(getClass().getClassLoader().getResourceAsStream("xss-policy.xml")) + .map(inputStream -> { + try { + return Policy.getInstance(inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .orElseThrow(() -> new IllegalStateException("XSS policy file not found")); } } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { - if (!(value instanceof String) || ((String) value).isEmpty() || xssPolicy == null) { + if (!(value instanceof String) || ((String) value).isEmpty()) { return true; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index b422f1cd4a..446125e4fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -50,23 +50,41 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))", - countQuery = "SELECT count(a) FROM AlarmEntity a " + - "LEFT JOIN RelationEntity re ON a.id = re.toId " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + , + countQuery = "" + + "SELECT count(a) + " + //alarms with relations only + " (SELECT count(a) FROM AlarmEntity a " + //alarms WITHOUT any relations + " LEFT JOIN RelationEntity re ON a.id = re.toId " + + " AND re.relationTypeGroup = 'ALARM' " + + " AND re.toType = 'ALARM' " + + " AND re.fromId = :affectedEntityId " + + " AND re.fromType = :affectedEntityType " + + " WHERE a.tenantId = :tenantId " + + " AND (a.originatorId = :affectedEntityId) " + + " AND (re.fromId IS NULL) " + //anti join + " AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + " AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + " AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + " AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + + " )" + + "FROM AlarmEntity a " + + "INNER JOIN RelationEntity re ON a.id = re.toId " + "AND re.relationTypeGroup = 'ALARM' " + "AND re.toType = 'ALARM' " + "AND re.fromId = :affectedEntityId " + "AND re.fromType = :affectedEntityType " + "WHERE a.tenantId = :tenantId " + - "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))") + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") Page findAlarms(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java index c6c545b574..f4eb71eaed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java @@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); try { componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + transactionManager.commit(transaction); } catch (Throwable th) { log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); transactionManager.rollback(transaction); } - transactionManager.commit(transaction); } else { log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); } 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 b98a57d248..c0a6d03226 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 @@ -190,7 +190,18 @@ public interface DeviceRepository extends PagingAndSortingRepository + * There is two way to count devices. + * OPTIMAL: count(*) + * - returns _row_count_ and use index-only scan (super fast). + * SLOW: count(id) + * - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows. + * */ + @Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId") + Long countByTenantId(@Param("tenantId") UUID tenantId); @Query("SELECT d.id FROM DeviceEntity d " + "INNER JOIN DeviceProfileEntity p ON d.deviceProfileId = p.id " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java index fde9498702..93a33d1f9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java @@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); try { eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + transactionManager.commit(transaction); } catch (Throwable th) { log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); transactionManager.rollback(transaction); } - transactionManager.commit(transaction); } else { log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index c68d6f1a7e..270ab26329 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -44,11 +44,11 @@ public interface EventRepository extends PagingAndSortingRepository findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( - @Param("tenantId") UUID tenantId, - @Param("entityType") EntityType entityType, - @Param("entityId") UUID entityId, - @Param("eventType") String eventType, - Pageable pageable); + @Param("tenantId") UUID tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") UUID entityId, + @Param("eventType") String eventType, + Pageable pageable); @Query("SELECT e FROM EventEntity e WHERE " + "e.tenantId = :tenantId " + @@ -80,4 +80,165 @@ public interface EventRepository extends PagingAndSortingRepository= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:type IS NULL OR lower(json_body->>'type') LIKE concat('%', lower(:type\\:\\:varchar), '%')) " + + "AND (:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:entityName IS NULL OR lower(json_body->>'entityName') LIKE concat('%', lower(:entityName\\:\\:varchar), '%')) " + + "AND (:relationType IS NULL OR lower(json_body->>'relationType') LIKE concat('%', lower(:relationType\\:\\:varchar), '%')) " + + "AND (:bodyEntityId IS NULL OR lower(json_body->>'entityId') LIKE concat('%', lower(:bodyEntityId\\:\\:varchar), '%')) " + + "AND (:msgType IS NULL OR lower(json_body->>'msgType') LIKE concat('%', lower(:msgType\\:\\:varchar), '%')) " + + "AND ((:isError = FALSE) OR (json_body->>'error') IS NOT NULL) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%')) " + + "AND (:data IS NULL OR lower(json_body->>'data') LIKE concat('%', lower(:data\\:\\:varchar), '%')) " + + "AND (:metadata IS NULL OR lower(json_body->>'metadata') LIKE concat('%', lower(:metadata\\:\\:varchar), '%')) ", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = :eventType " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:type IS NULL OR lower(json_body->>'type') LIKE concat('%', lower(:type\\:\\:varchar), '%')) " + + "AND (:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:entityName IS NULL OR lower(json_body->>'entityName') LIKE concat('%', lower(:entityName\\:\\:varchar), '%')) " + + "AND (:relationType IS NULL OR lower(json_body->>'relationType') LIKE concat('%', lower(:relationType\\:\\:varchar), '%')) " + + "AND (:bodyEntityId IS NULL OR lower(json_body->>'entityId') LIKE concat('%', lower(:bodyEntityId\\:\\:varchar), '%')) " + + "AND (:msgType IS NULL OR lower(json_body->>'msgType') LIKE concat('%', lower(:msgType\\:\\:varchar), '%')) " + + "AND ((:isError = FALSE) OR (json_body->>'error') IS NOT NULL) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%')) " + + "AND (:data IS NULL OR lower(json_body->>'data') LIKE concat('%', lower(:data\\:\\:varchar), '%')) " + + "AND (:metadata IS NULL OR lower(json_body->>'metadata') LIKE concat('%', lower(:metadata\\:\\:varchar), '%'))" + ) + Page findDebugRuleNodeEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("eventType") String eventType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("type") String type, + @Param("server") String server, + @Param("entityName") String entityName, + @Param("relationType") String relationType, + @Param("bodyEntityId") String bodyEntityId, + @Param("msgType") String msgType, + @Param("isError") boolean isError, + @Param("error") String error, + @Param("data") String data, + @Param("metadata") String metadata, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'ERROR' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:method IS NULL OR lower(json_body->>'method') LIKE concat('%', lower(:method\\:\\:varchar), '%')) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'ERROR' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:method IS NULL OR lower(json_body->>'method') LIKE concat('%', lower(:method\\:\\:varchar), '%')) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))") + Page findErrorEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("method") String method, + @Param("error") String error, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:event IS NULL OR lower(json_body->>'event') LIKE concat('%', lower(:event\\:\\:varchar), '%')) " + + "AND ((:statusFilterEnabled = FALSE) OR lower(json_body->>'success')\\:\\:boolean = :statusFilter) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))" + , + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:event IS NULL OR lower(json_body->>'event') LIKE concat('%', lower(:event\\:\\:varchar), '%')) " + + "AND ((:statusFilterEnabled = FALSE) OR lower(json_body->>'success')\\:\\:boolean = :statusFilter) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))" + ) + Page findLifeCycleEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("event") String event, + @Param("statusFilterEnabled") boolean statusFilterEnabled, + @Param("statusFilter") boolean statusFilter, + @Param("error") String error, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'STATS' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(e.body\\:\\:json->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:messagesProcessed = 0 OR (json_body->>'messagesProcessed')\\:\\:integer >= :messagesProcessed) " + + "AND (:errorsOccurred = 0 OR (json_body->>'errorsOccurred')\\:\\:integer >= :errorsOccurred) ", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(e.body\\:\\:json->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:messagesProcessed = 0 OR (json_body->>'messagesProcessed')\\:\\:integer >= :messagesProcessed) " + + "AND (:errorsOccurred = 0 OR (json_body->>'errorsOccurred')\\:\\:integer >= :errorsOccurred) ") + Page findStatisticsEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("messagesProcessed") Integer messagesProcessed, + @Param("errorsOccurred") Integer errorsOccurred, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 06f54042d7..44848ec515 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -24,6 +24,12 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.DebugEvent; +import org.thingsboard.server.common.data.event.ErrorEventFilter; +import org.thingsboard.server.common.data.event.EventFilter; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.event.LifeCycleEventFilter; +import org.thingsboard.server.common.data.event.StatisticsEventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.TenantId; @@ -147,6 +153,98 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen DaoUtil.toPageable(pageLink))); } + @Override + public PageData findEventByFilter(UUID tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { + if (eventFilter.hasFilterForJsonBody()) { + switch (eventFilter.getEventType()) { + case DEBUG_RULE_NODE: + case DEBUG_RULE_CHAIN: + return findEventByFilter(tenantId, entityId, (DebugEvent) eventFilter, pageLink); + case LC_EVENT: + return findEventByFilter(tenantId, entityId, (LifeCycleEventFilter) eventFilter, pageLink); + case ERROR: + return findEventByFilter(tenantId, entityId, (ErrorEventFilter) eventFilter, pageLink); + case STATS: + return findEventByFilter(tenantId, entityId, (StatisticsEventFilter) eventFilter, pageLink); + default: + throw new RuntimeException("Not supported event type: " + eventFilter.getEventType()); + } + } else { + return findEvents(tenantId, entityId, eventFilter.getEventType().name(), pageLink); + } + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, DebugEvent eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findDebugRuleNodeEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + eventFilter.getEventType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getMsgDirectionType(), + eventFilter.getServer(), + eventFilter.getEntityName(), + eventFilter.getRelationType(), + eventFilter.getEntityId(), + eventFilter.getMsgType(), + eventFilter.isError(), + eventFilter.getError(), + eventFilter.getDataSearch(), + eventFilter.getMetadataSearch(), + DaoUtil.toPageable(pageLink))); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, ErrorEventFilter eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findErrorEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + eventFilter.getMethod(), + eventFilter.getError(), + DaoUtil.toPageable(pageLink)) + ); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, LifeCycleEventFilter eventFilter, TimePageLink pageLink) { + boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + return DaoUtil.toPageData( + eventRepository.findLifeCycleEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + eventFilter.getEvent(), + statusFilterEnabled, + statusFilter, + eventFilter.getError(), + DaoUtil.toPageable(pageLink)) + ); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, StatisticsEventFilter eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findStatisticsEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + notNull(eventFilter.getMessagesProcessed()), + notNull(eventFilter.getErrorsOccurred()), + DaoUtil.toPageable(pageLink)) + ); + } + @Override public List findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { List latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( @@ -177,4 +275,12 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen return Optional.of(DaoUtil.getData(eventInsertRepository.saveOrUpdate(entity))); } + private long notNull(Long value) { + return value != null ? value : 0; + } + + private int notNull(Integer value) { + return value != null ? value : 0; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 89b63b87cf..5e10cabda2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -49,12 +49,17 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple private RelationInsertRepository relationInsertRepository; @Override - public ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { + return service.submit(() -> findAllByFrom(tenantId, from, typeGroup)); + } + + @Override + public List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByFromIdAndFromTypeAndRelationTypeGroup( from.getId(), from.getEntityType().name(), - typeGroup.name()))); + typeGroup.name())); } @Override @@ -68,12 +73,17 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { + return service.submit(() -> findAllByTo(tenantId, to, typeGroup)); + } + + @Override + public List findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByToIdAndToTypeAndRelationTypeGroup( to.getId(), to.getEntityType().name(), - typeGroup.name()))); + typeGroup.name())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java index 7a056f4872..5b18fde202 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sqlts.insert.latest.psql; +import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository; import org.springframework.transaction.TransactionStatus; @@ -37,20 +38,29 @@ import java.util.List; @Transactional public class PsqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { - private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? and key = ?"; + @Value("${sql.ts_latest.update_by_latest_ts:true}") + private Boolean updateByLatestTs; + private static final String BATCH_UPDATE = + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json);"; + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json)"; + + private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; + + private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?"; @Override public void saveOrUpdate(List entities) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { + String batchUpdateQuery = updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE; + String insertOrUpdateQuery = updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE; + + int[] result = jdbcTemplate.batchUpdate(batchUpdateQuery, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { TsKvLatestEntity tsKvLatestEntity = entities.get(i); @@ -80,6 +90,9 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple ps.setObject(7, tsKvLatestEntity.getEntityId()); ps.setInt(8, tsKvLatestEntity.getKey()); + if (updateByLatestTs) { + ps.setLong(9, tsKvLatestEntity.getTs()); + } } @Override @@ -102,7 +115,7 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple } } - jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + jdbcTemplate.batchUpdate(insertOrUpdateQuery, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); @@ -111,6 +124,9 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple ps.setLong(3, tsKvLatestEntity.getTs()); ps.setLong(9, tsKvLatestEntity.getTs()); + if (updateByLatestTs) { + ps.setLong(15, tsKvLatestEntity.getTs()); + } if (tsKvLatestEntity.getBooleanValue() != null) { ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); 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 9a08c65fef..b514b43cf3 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 @@ -35,7 +35,7 @@ import org.thingsboard.server.dao.device.DeviceService; 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.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -90,7 +90,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe private RuleChainService ruleChainService; @Autowired - private TbResourceService resourceService; + private ResourceService resourceService; @Override public Tenant findTenantById(TenantId 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 7d09578dd1..240d5a0b88 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 @@ -62,6 +62,8 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement[] fetchStmtsDesc; private PreparedStatement deleteStmt; private PreparedStatement deletePartitionStmt; + private final Lock stmtCreationLock = new ReentrantLock(); private boolean isInstall() { return environment.acceptsProfiles(Profiles.of("install")); @@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getDeleteStmt() { if (deleteStmt == null) { - deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "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 + " < ?"); + stmtCreationLock.lock(); + try { + if (deleteStmt == null) { + deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "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 + " < ?"); + } + } finally { + stmtCreationLock.unlock(); + } } return deleteStmt; } @@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getDeletePartitionStmt() { if (deletePartitionStmt == null) { - deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF + - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); + stmtCreationLock.lock(); + try { + if (deletePartitionStmt == null) { + deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); + } + } finally { + stmtCreationLock.unlock(); + } } return deletePartitionStmt; } private PreparedStatement getSaveStmt(DataType dataType) { if (saveStmts == null) { - saveStmts = new PreparedStatement[DataType.values().length]; - for (DataType type : DataType.values()) { - saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"); + stmtCreationLock.lock(); + try { + if (saveStmts == null) { + saveStmts = new PreparedStatement[DataType.values().length]; + for (DataType type : DataType.values()) { + saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + } + } finally { + stmtCreationLock.unlock(); } } return saveStmts[dataType.ordinal()]; @@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getSaveTtlStmt(DataType dataType) { if (saveTtlStmts == null) { - saveTtlStmts = new PreparedStatement[DataType.values().length]; - for (DataType type : DataType.values()) { - saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); + stmtCreationLock.lock(); + try { + if (saveTtlStmts == null) { + saveTtlStmts = new PreparedStatement[DataType.values().length]; + for (DataType type : DataType.values()) { + saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); + } + } + } finally { + stmtCreationLock.unlock(); } } return saveTtlStmts[dataType.ordinal()]; @@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getPartitionInsertStmt() { if (partitionInsertStmt == null) { - partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?)"); + stmtCreationLock.lock(); + try { + if (partitionInsertStmt == null) { + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?)"); + } + } finally { + stmtCreationLock.unlock(); + } } return partitionInsertStmt; } private PreparedStatement getPartitionInsertTtlStmt() { if (partitionInsertTtlStmt == null) { - partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?) USING TTL ?"); + stmtCreationLock.lock(); + try { + if (partitionInsertTtlStmt == null) { + partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?) USING TTL ?"); + } + } finally { + stmtCreationLock.unlock(); + } } return partitionInsertTtlStmt; } @@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD switch (orderBy) { case ASC_ORDER: if (fetchStmtsAsc == null) { - fetchStmtsAsc = initFetchStmt(orderBy); + stmtCreationLock.lock(); + try { + if (fetchStmtsAsc == null) { + fetchStmtsAsc = initFetchStmt(orderBy); + } + } finally { + stmtCreationLock.unlock(); + } } return fetchStmtsAsc[aggType.ordinal()]; case DESC_ORDER: if (fetchStmtsDesc == null) { - fetchStmtsDesc = initFetchStmt(orderBy); + stmtCreationLock.lock(); + try { + if (fetchStmtsDesc == null) { + fetchStmtsDesc = initFetchStmt(orderBy); + } + } finally { + stmtCreationLock.unlock(); + } } return fetchStmtsDesc[aggType.ordinal()]; default: 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 cfd232b330..327b05ee22 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 @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.Tenant; 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; @@ -54,7 +55,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -70,6 +71,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import static org.junit.Assert.assertNotNull; + @RunWith(SpringRunner.class) @ContextConfiguration(classes = AbstractServiceTest.class, loader = AnnotationConfigContextLoader.class) @@ -152,9 +155,9 @@ public abstract class AbstractServiceTest { protected DeviceProfileService deviceProfileService; @Autowired - protected TbResourceService resourceService; + protected ResourceService resourceService; - class IdComparator implements Comparator { + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); @@ -225,4 +228,12 @@ public abstract class AbstractServiceTest { return deviceProfile; } + public TenantId createTenant() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + Uuids.timeBased()); + Tenant savedTenant = tenantService.saveTenant(tenant); + assertNotNull(savedTenant); + return savedTenant.getId(); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index b323d0088c..16f7ab2868 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -21,13 +21,19 @@ 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.DeviceInfo; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; 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.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -37,33 +43,93 @@ import java.util.List; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; public abstract class BaseDeviceServiceTest extends AbstractServiceTest { - + private IdComparator idComparator = new IdComparator<>(); - + private TenantId tenantId; + private TenantId anotherTenantId; @Before public void before() { - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - Tenant savedTenant = tenantService.saveTenant(tenant); - Assert.assertNotNull(savedTenant); - tenantId = savedTenant.getId(); + tenantId = createTenant(); + anotherTenantId = createTenant(); } @After public void after() { tenantService.deleteTenant(tenantId); + tenantService.deleteTenant(anotherTenantId); + + tenantProfileService.deleteTenantProfiles(tenantId); + tenantProfileService.deleteTenantProfiles(anotherTenantId); + } + + @Test + public void testSaveDevicesWithoutMaxDeviceLimit() { + Device device = this.saveDevice(tenantId, "My device"); + deleteDevice(tenantId, device); } @Test - public void testSaveDevice() { + public void testSaveDevicesWithInfiniteMaxDeviceLimit() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(Long.MAX_VALUE).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + Device device = this.saveDevice(tenantId, "My device"); + deleteDevice(tenantId, device); + } + + @Test(expected = DataValidationException.class) + public void testSaveDevicesWithMaxDeviceOutOfLimit() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(1).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + + this.saveDevice(tenantId, "My first device"); + Assert.assertEquals(1, deviceService.countByTenantId(tenantId)); + + this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + } + + @Test + public void testCountByTenantId() { + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(anotherTenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(TenantId.SYS_TENANT_ID)); + + Device anotherDevice = this.saveDevice(anotherTenantId, "My device 1"); + Assert.assertEquals(1, deviceService.countByTenantId(anotherTenantId)); + + int maxDevices = 8; + List devices = new ArrayList<>(maxDevices); + + for (int i = 1; i <= maxDevices; i++) { + devices.add(this.saveDevice(tenantId, "My device " + i)); + Assert.assertEquals(i, deviceService.countByTenantId(tenantId)); + } + + Assert.assertEquals(maxDevices, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(1, deviceService.countByTenantId(anotherTenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(TenantId.SYS_TENANT_ID)); + + devices.forEach(device -> deleteDevice(tenantId, device)); + deleteDevice(anotherTenantId, anotherDevice); + } + + void deleteDevice(TenantId tenantId, Device device) { + deviceService.deleteDevice(tenantId, device.getId()); + } + + Device saveDevice(TenantId tenantId, final String name) { Device device = new Device(); device.setTenantId(tenantId); - device.setName("My device"); + device.setName(name); device.setType("default"); Device savedDevice = deviceService.saveDevice(device); - + Assert.assertNotNull(savedDevice); Assert.assertNotNull(savedDevice.getId()); Assert.assertTrue(savedDevice.getCreatedTime() > 0); @@ -71,7 +137,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertNotNull(savedDevice.getCustomerId()); Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId()); Assert.assertEquals(device.getName(), savedDevice.getName()); - + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); Assert.assertNotNull(deviceCredentials); Assert.assertNotNull(deviceCredentials.getId()); @@ -79,16 +145,15 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType()); Assert.assertNotNull(deviceCredentials.getCredentialsId()); Assert.assertEquals(20, deviceCredentials.getCredentialsId().length()); - - savedDevice.setName("My new device"); - + + savedDevice.setName("New " + savedDevice.getName()); + deviceService.saveDevice(savedDevice); Device foundDevice = deviceService.findDeviceById(tenantId, savedDevice.getId()); Assert.assertEquals(foundDevice.getName(), savedDevice.getName()); - - deviceService.deleteDevice(tenantId, savedDevice.getId()); + return foundDevice; } - + @Test(expected = DataValidationException.class) public void testSaveDeviceWithEmptyName() { Device device = new Device(); @@ -96,7 +161,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setTenantId(tenantId); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testSaveDeviceWithEmptyTenant() { Device device = new Device(); @@ -104,7 +169,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); @@ -113,7 +178,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setTenantId(new TenantId(Uuids.timeBased())); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); @@ -149,7 +214,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenant.getId()); } } - + @Test public void testFindDeviceById() { Device device = new Device(); @@ -198,7 +263,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { devices.forEach((device) -> { deviceService.deleteDevice(tenantId, device.getId()); }); } } - + @Test public void testDeleteDevice() { Device device = new Device(); @@ -214,15 +279,15 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); Assert.assertNull(foundDeviceCredentials); } - + @Test public void testFindDevicesByTenantId() { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); - + TenantId tenantId = tenant.getId(); - + List devices = new ArrayList<>(); for (int i=0;i<178;i++) { Device device = new Device(); @@ -231,7 +296,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); devices.add(deviceService.saveDevice(device)); } - + List loadedDevices = new ArrayList<>(); PageLink pageLink = new PageLink(23); PageData pageData = null; @@ -242,19 +307,19 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devices, idComparator); Collections.sort(loadedDevices, idComparator); - + Assert.assertEquals(devices, loadedDevices); - + deviceService.deleteDevicesByTenantId(tenantId); pageLink = new PageLink(33); pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } @@ -284,7 +349,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } - + List loadedDevicesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); PageData pageData = null; @@ -295,12 +360,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devicesTitle1, idComparator); Collections.sort(loadedDevicesTitle1, idComparator); - + Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); - + List loadedDevicesTitle2 = new ArrayList<>(); pageLink = new PageLink(4, 0, title2); do { @@ -313,22 +378,22 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Collections.sort(devicesTitle2, idComparator); Collections.sort(loadedDevicesTitle2, idComparator); - + Assert.assertEquals(devicesTitle2, loadedDevicesTitle2); for (Device device : loadedDevicesTitle1) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title1); pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Device device : loadedDevicesTitle2) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title2); pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); @@ -413,21 +478,21 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } - + @Test public void testFindDevicesByTenantIdAndCustomerId() { 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 devices = new ArrayList<>(); for (int i=0;i<278;i++) { Device device = new Device(); @@ -437,7 +502,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); } - + List loadedDevices = new ArrayList<>(); PageLink pageLink = new PageLink(23); PageData pageData = null; @@ -448,31 +513,31 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devices, idComparator); Collections.sort(loadedDevices, idComparator); - + Assert.assertEquals(devices, loadedDevices); - + deviceService.unassignCustomerDevices(tenantId, customerId); pageLink = new PageLink(33); pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } - + @Test public void testFindDevicesByTenantIdCustomerIdAndName() { - + Customer customer = new Customer(); customer.setTitle("Test customer"); customer.setTenantId(tenantId); customer = customerService.saveCustomer(customer); CustomerId customerId = customer.getId(); - + String title1 = "Device title 1"; List devicesTitle1 = new ArrayList<>(); for (int i=0;i<175;i++) { @@ -499,7 +564,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); devicesTitle2.add(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId)); } - + List loadedDevicesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); PageData pageData = null; @@ -510,12 +575,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devicesTitle1, idComparator); Collections.sort(loadedDevicesTitle1, idComparator); - + Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); - + List loadedDevicesTitle2 = new ArrayList<>(); pageLink = new PageLink(4, 0, title2); do { @@ -528,22 +593,22 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Collections.sort(devicesTitle2, idComparator); Collections.sort(loadedDevicesTitle2, idComparator); - + Assert.assertEquals(devicesTitle2, loadedDevicesTitle2); for (Device device : loadedDevicesTitle1) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title1); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Device device : loadedDevicesTitle2) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title2); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); 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 e7cd349e73..3f4b103ce2 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 @@ -292,7 +292,8 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleNodes.set(name3Index, ruleNode4); - RuleChainMetaData updatedRuleChainMetaData = ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData)); + RuleChainMetaData updatedRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, savedRuleChainMetaData.getRuleChainId()); Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size()); Assert.assertEquals(3, updatedRuleChainMetaData.getConnections().size()); @@ -404,7 +405,8 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.addConnectionInfo(0,2,"fail"); ruleChainMetaData.addConnectionInfo(1,2,"success"); - return ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); } private RuleChainMetaData createRuleChainMetadataWithCirclingRelation() throws Exception { diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index 044fe00255..98718a201b 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb As result, in REPOSITORY column, next images should be present: thingsboard/tb-coap-transport + thingsboard/tb-lwm2m-transport thingsboard/tb-http-transport thingsboard/tb-mqtt-transport thingsboard/tb-node diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index f9eddd420c..ec92116c81 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -17,15 +17,14 @@ FROM thingsboard/openjdk11 RUN apt-get update -RUN apt-get install -y curl nmap procps -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN apt-get install -y curl nmap procps gnupg2 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add - -ENV PG_MAJOR=11 +ENV PG_MAJOR=12 RUN apt-get update -RUN apt-get install -y cassandra cassandra-tools postgresql-11 +RUN apt-get install -y cassandra cassandra-tools postgresql-12 RUN update-rc.d cassandra disable RUN update-rc.d postgresql disable RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 9b314a5a5d..7221bc08c6 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -17,13 +17,12 @@ FROM thingsboard/openjdk11 RUN apt-get update -RUN apt-get install -y curl -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN apt-get install -y curl gnupg2 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -ENV PG_MAJOR 11 +ENV PG_MAJOR 12 RUN apt-get update -RUN apt-get install -y postgresql-11 +RUN apt-get install -y postgresql-12 RUN update-rc.d postgresql disable COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index efb9a709e9..a19c2c542f 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -39,7 +39,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 2.4.2 + 3.3.0 diff --git a/pom.xml b/pom.xml index ec1df8c7e6..4073fe824e 100755 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 4.2.0 3.5.5 3.11.4 - 1.22.1 + 1.36.0 1.18.18 1.2.4 4.1.60.Final @@ -99,7 +99,7 @@ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 - 0.1.31 + 0.1.16 2.6.0 4.1.1 2.57 @@ -1307,6 +1307,11 @@ protobuf-java ${protobuf.version} + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + io.grpc grpc-netty 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 de7ba29141..e51117c080 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 @@ -121,7 +121,6 @@ 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; @@ -306,16 +305,24 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public PageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarm/{entityType}/{entityId}?fetchOriginator={fetchOriginator}"; Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); - params.put("searchStatus", searchStatus.name()); - params.put("status", status.name()); params.put("fetchOriginator", String.valueOf(fetchOriginator)); + if(searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if(status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + addTimePageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getTimeUrlParams(pageLink), + baseURL + urlSecondPart + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -1338,7 +1345,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); } - public PageData getTenantDevices(PageLink pageLink) { + public PageData getDeviceProfiles(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( @@ -2841,7 +2848,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } private String getTimeUrlParams(TimePageLink pageLink) { - String urlParams = "limit={limit}&ascOrder={ascOrder}"; + String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { urlParams += "&startTime={startTime}"; } 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 index 79ced87bd7..e766c2600c 100644 --- 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 @@ -16,13 +16,14 @@ package org.thingsboard.rule.engine.edge; 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.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.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -87,8 +88,6 @@ public class TbMsgPushToEdgeNode implements TbNode { private EmptyNodeConfiguration config; - private static final ObjectMapper json = new ObjectMapper(); - private static final String SCOPE = "scope"; @Override @@ -200,7 +199,7 @@ public class TbMsgPushToEdgeNode implements TbNode { EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType); Map entityBody = new HashMap<>(); Map metadata = msg.getMetaData().getData(); - JsonNode dataJson = json.readTree(msg.getData()); + JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); switch (actionType) { case ATTRIBUTES_UPDATED: case POST_ATTRIBUTES: @@ -208,7 +207,7 @@ public class TbMsgPushToEdgeNode implements TbNode { entityBody.put(SCOPE, getScope(metadata)); break; case ATTRIBUTES_DELETED: - List keys = json.treeToValue(dataJson.get("attributes"), List.class); + List keys = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() {}); entityBody.put("keys", keys); entityBody.put(SCOPE, getScope(metadata)); break; @@ -217,7 +216,7 @@ public class TbMsgPushToEdgeNode implements TbNode { entityBody.put("ts", metadata.get("ts")); break; } - return buildEdgeEvent(ctx.getTenantId(), actionType, msg.getOriginator().getId(), edgeEventTypeByEntityType, json.valueToTree(entityBody)); + return buildEdgeEvent(ctx.getTenantId(), actionType, msg.getOriginator().getId(), edgeEventTypeByEntityType, JacksonUtil.valueToTree(entityBody)); } } @@ -240,9 +239,9 @@ public class TbMsgPushToEdgeNode implements TbNode { 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); + private UUID getUUIDFromMsgData(TbMsg msg) { + JsonNode data = JacksonUtil.toJsonNode(msg.getData()).get("id"); + String id = JacksonUtil.convertValue(data.get("id"), String.class); return UUID.fromString(id); } diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index ec63dcae75..922aed5450 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -119,6 +119,18 @@ redis: # LWM2M server parameters transport: + sessions: + inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" + report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" + json: + # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON + type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:false}" + # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) + max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + client_side_rpc: + timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" + # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) + api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" # Local LwM2M transport parameters lwm2m: # Enable/disable lvm2m transport protocol. @@ -180,15 +192,6 @@ transport: # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" - sessions: - inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" - report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - json: - # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON - type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" - # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) - max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" - queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: diff --git a/ui-ngx/src/app/core/http/event.service.ts b/ui-ngx/src/app/core/http/event.service.ts index 9bf1b4e86f..fd740d73a7 100644 --- a/ui-ngx/src/app/core/http/event.service.ts +++ b/ui-ngx/src/app/core/http/event.service.ts @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { TimePageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { DebugEventType, Event, EventType } from '@shared/models/event.models'; +import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -39,4 +39,10 @@ export class EventService { defaultHttpOptionsFromConfig(config)); } + public getFilterEvents(entityId: EntityId, eventType: EventType | DebugEventType, tenantId: string, + filters: FilterEventBody, pageLink: TimePageLink, config?: RequestConfig): Observable> { + return this.http.post>(`/api/events/${entityId.entityType}/${entityId.id}` + + `${pageLink.toQuery()}&tenantId=${tenantId}`, {...filters, eventType}, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index d8bc18291b..2fef72fedb 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo } from '@shared/models/resource.models'; -import { map } from 'rxjs/operators'; +import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -43,14 +43,17 @@ export class ResourceService { } public downloadResource(resourceId: string): Observable { - return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe( + return this.http.get(`/api/resource/${resourceId}/download`, { + responseType: 'arraybuffer', + observe: 'response' + }).pipe( map((response) => { const headers = response.headers; const filename = headers.get('x-filename'); const contentType = headers.get('content-type'); const linkElement = document.createElement('a'); try { - const blob = new Blob([response.body], { type: contentType }); + const blob = new Blob([response.body], {type: contentType}); const url = URL.createObjectURL(blob); linkElement.setAttribute('href', url); linkElement.setAttribute('download', filename); @@ -70,6 +73,25 @@ export class ResourceService { ); } + public saveResources(resources: Resource[], config?: RequestConfig): Observable { + let partSize = 100; + partSize = resources.length > partSize ? partSize : resources.length; + const resourceObservables: Observable[] = []; + for (let i = 0; i < partSize; i++) { + resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.saveResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + public saveResource(resource: Resource, config?: RequestConfig): Observable { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts index 47c8d8a456..23fc215573 100644 --- a/ui-ngx/src/app/core/services/time.service.ts +++ b/ui-ngx/src/app/core/services/time.service.ts @@ -31,7 +31,7 @@ import { isDefined } from '@core/utils'; export interface TimeInterval { name: string; - translateParams: {[key: string]: any}; + translateParams: { [key: string]: any }; value: number; } @@ -56,14 +56,14 @@ export class TimeService { public loadMaxDatapointsLimit(): Observable { return this.http.get('/api/dashboard/maxDatapointsLimit', defaultHttpOptions(true)).pipe( - map( (limit) => { - this.maxDatapointsLimit = limit; - if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { - this.maxDatapointsLimit = MIN_LIMIT + 1; - } - return this.maxDatapointsLimit; - }) - ); + map((limit) => { + this.maxDatapointsLimit = limit; + if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { + this.maxDatapointsLimit = MIN_LIMIT + 1; + } + return this.maxDatapointsLimit; + }) + ); } public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean { @@ -79,7 +79,7 @@ export class TimeService { public boundMinInterval(min: number): number { if (isDefined(min)) { - min = Math.floor(min / 1000) * 1000; + min = Math.ceil(min / 1000) * 1000; } return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b9c2697d0a..b2d7b8b2a7 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { return true; } +export function isLiteralObject(value: any) { + return (!!value) && (value.constructor === Object); +} + export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { if (isDefinedAndNotNull(value) && isNumeric(value) && (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { @@ -287,7 +291,7 @@ export function deepClone(target: T, ignoreFields?: string[]): T { return cp.map((n: any) => deepClone(n)) as any; } if (typeof target === 'object' && target !== {}) { - const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; + const cp = {...(target as { [key: string]: any })} as { [key: string]: any }; Object.keys(cp).forEach(k => { if (!ignoreFields || ignoreFields.indexOf(k) === -1) { cp[k] = deepClone(cp[k]); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 762b7149a2..3f03ea4fff 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -56,6 +56,8 @@ [syncStateWithQueryParam]="syncStateWithQueryParam" [states]="dashboardConfiguration.states"> +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss index c7d2367975..6ace69d312 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss @@ -54,6 +54,11 @@ div.tb-dashboard-page { z-index: 13; pointer-events: none; + .dashboard_logo{ + height: 75%; + margin-right: 16px; + } + &.tb-dashboard-toolbar-opened { right: 0; // transition: right .3s cubic-bezier(.55, 0, .55, .2); 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 7e760e3337..2b325e1aa0 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 @@ -90,7 +90,6 @@ import { } from '@home/components/alias/entity-aliases-dialog.component'; import { EntityAliases } from '@app/shared/models/alias.models'; import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component'; -import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { AddWidgetDialogComponent, AddWidgetDialogData @@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal'; import { DISPLAY_WIDGET_TYPES_PANEL_DATA, DisplayWidgetTypesPanelComponent, - DisplayWidgetTypesPanelData, - WidgetTypes + DisplayWidgetTypesPanelData } 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"; @@ -193,6 +191,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC addingLayoutCtx: DashboardPageLayoutContext; + logo = 'assets/logo_title_white.svg'; dashboardCtx: DashboardContext = { instanceId: this.utils.guid(), @@ -486,6 +485,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } + public showDashboardLogo(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showDashboardLogo)) { + return this.dashboard.configuration.settings.showDashboardLogo && (this.forceFullscreen || this.singlePageMode || this.isFullscreen); + } else { + return false; + } + } + + public get dashboardLogo(): string { + return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo; + } + public showRightLayoutSwitch(): boolean { return this.isMobile && this.layouts.right.show; } @@ -607,7 +619,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { settings: deepClone(this.dashboard.configuration.settings), - gridSettings + gridSettings, } }).afterClosed().subscribe((data) => { if (data) { @@ -1189,13 +1201,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC overlayRef.dispose(); }); + const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes; + const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => { + return {type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)}; + }); + const providers: StaticProvider[] = [ { provide: DISPLAY_WIDGET_TYPES_PANEL_DATA, useValue: { - types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => { - return {type, display: true}; - }), + types: widgetTypesList, typesUpdated: (newTypes) => { this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index ede8052c68..244045314b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -52,7 +52,8 @@ formControlName="titleColor">
-
+
{{ 'dashboard.display-dashboards-selection' | translate }} @@ -69,6 +70,13 @@ {{ 'dashboard.display-dashboard-export' | translate }}
+ + {{ 'dashboard.display-dashboard-logo' | translate }} + + +
{ + return this.filterWidgetTypes$.value; + } + @Output() widgetSelected: EventEmitter = new EventEmitter(); 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 d893ec848f..c050b524cf 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,7 +80,7 @@ (mousedown)="widgetMouseDown($event, widget)" (click)="widgetClicked($event, widget)" (contextmenu)="openWidgetContextMenu($event, widget)"> -
+
+
+ + + + + {{ column.title | translate}} + + {{ 'event.all-events' | translate}} + + {{ value }} + + + + + + + {{ column.title | translate}} + + + {{ 'event.min-value' | translate }} + + + + + + {{ 'event.has-error' | translate }} + + + + + {{ column.title | translate}} + + + + + + {{ column.title | translate}} + + + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss new file mode 100644 index 0000000000..971b81689f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss @@ -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. + */ +:host { + width: 100%; + min-width: 300px; + overflow: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts new file mode 100644 index 0000000000..f60f7a09f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts @@ -0,0 +1,109 @@ +/// +/// 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, InjectionToken } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { EntityType } from '@shared/models/entity-type.models'; +import { FilterEventBody } from '@shared/models/event.models'; +import { deepTrim } from '@core/utils'; + +export const EVENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface EventFilterPanelData { + filterParams: FilterEventBody; + columns: Array; +} + +export interface FilterEntityColumn { + key: string; + title: string; +} + + +@Component({ + selector: 'tb-event-filter-panel', + templateUrl: './event-filter-panel.component.html', + styleUrls: ['./event-filter-panel.component.scss'] +}) +export class EventFilterPanelComponent { + + eventFilterFormGroup: FormGroup; + result: EventFilterPanelData; + + private conditionError = false; + + private msgDirectionTypes = ['IN', 'OUT']; + private statusTypes = ['Success', 'Failure']; + private entityTypes = Object.keys(EntityType); + + showColumns: FilterEntityColumn[] = []; + + constructor(@Inject(EVENT_FILTER_PANEL_DATA) + public data: EventFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder) { + this.eventFilterFormGroup = this.fb.group({}); + this.data.columns.forEach((column) => { + this.showColumns.push(column); + const validators = []; + if (this.isNumberFields(column.key)) { + validators.push(Validators.min(0)); + } + this.eventFilterFormGroup.addControl(column.key, this.fb.control(this.data.filterParams[column.key] || '', validators)); + if (column.key === 'isError') { + this.conditionError = true; + } + }); + } + + isSelector(key: string): string { + return ['msgDirectionType', 'status', 'entityName'].includes(key) ? key : ''; + } + + isNumberFields(key: string): string { + return ['messagesProcessed', 'errorsOccurred'].includes(key) ? key : ''; + } + + selectorValues(key: string): string[] { + switch (key) { + case 'msgDirectionType': + return this.msgDirectionTypes; + case 'status': + return this.statusTypes; + case 'entityName': + return this.entityTypes; + } + } + + update() { + const filter = deepTrim(Object.fromEntries(Object.entries(this.eventFilterFormGroup.value).filter(([_, v]) => v !== ''))); + this.result = { + filterParams: filter, + columns: this.data.columns + }; + this.overlayRef.dispose(); + } + + showErrorMsgFields() { + return !this.conditionError || this.eventFilterFormGroup.get('isError').value !== ''; + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index f18df5cd02..1dc663684f 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -20,7 +20,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { DebugEventType, Event, EventType } from '@shared/models/event.models'; +import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; import { TimePageLink } from '@shared/models/page/page-link'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -38,16 +38,29 @@ import { EventContentDialogComponent, EventContentDialogData } from '@home/components/event/event-content-dialog.component'; -import { sortObjectKeys } from '@core/utils'; +import { isEqual, sortObjectKeys } from '@core/utils'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { + EVENT_FILTER_PANEL_DATA, + EventFilterPanelComponent, + EventFilterPanelData, + FilterEntityColumn +} from '@home/components/event/event-filter-panel.component'; export class EventTableConfig extends EntityTableConfig { eventTypeValue: EventType | DebugEventType; + private filterParams: FilterEventBody = {}; + private filterColumns: FilterEntityColumn[] = []; + set eventType(eventType: EventType | DebugEventType) { if (this.eventTypeValue !== eventType) { this.eventTypeValue = eventType; this.updateColumns(true); + this.updateFilterColumns(); } } @@ -66,7 +79,10 @@ export class EventTableConfig extends EntityTableConfig { public tenantId: string, private defaultEventType: EventType | DebugEventType, private disabledEventTypes: Array = null, - private debugEventTypes: Array = null) { + private debugEventTypes: Array = null, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { super(); this.loadDataOnInit = false; this.tableTitle = ''; @@ -101,10 +117,20 @@ export class EventTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.updateColumns(); + this.updateFilterColumns(); + + this.headerActionDescriptors.push({ + name: this.translate.instant('event.events-filter'), + icon: 'filter_list', + isEnabled: () => true, + onAction: ($event) => { + this.editEventFilter($event); + } + }); } fetchEvents(pageLink: TimePageLink): Observable> { - return this.eventService.getEvents(this.entityId, this.eventType, this.tenantId, pageLink); + return this.eventService.getFilterEvents(this.entityId, this.eventType, this.tenantId, this.filterParams, pageLink); } updateColumns(updateTableColumns: boolean = false): void { @@ -169,7 +195,7 @@ export class EventTableConfig extends EntityTableConfig { }), false, key => ({ padding: '0 12px 0 0' })), - new EntityTableColumn('entity', 'event.entity', '100px', + new EntityTableColumn('entityName', 'event.entity-type', '100px', (entity) => entity.body.entityName, entity => ({ padding: '0 12px 0 0', }), false, key => ({ @@ -249,5 +275,91 @@ export class EventTableConfig extends EntityTableConfig { } }); } + + private updateFilterColumns() { + this.filterParams = {}; + this.filterColumns = [{key: 'server', title: 'event.server'}]; + switch (this.eventType) { + case EventType.ERROR: + this.filterColumns.push( + {key: 'method', title: 'event.method'}, + {key: 'error', title: 'event.error'} + ); + break; + case EventType.LC_EVENT: + this.filterColumns.push( + {key: 'method', title: 'event.event'}, + {key: 'status', title: 'event.status'}, + {key: 'error', title: 'event.error'} + ); + break; + case EventType.STATS: + this.filterColumns.push( + {key: 'messagesProcessed', title: 'event.min-messages-processed'}, + {key: 'errorsOccurred', title: 'event.min-errors-occurred'} + ); + break; + case DebugEventType.DEBUG_RULE_NODE: + case DebugEventType.DEBUG_RULE_CHAIN: + this.filterColumns.push( + {key: 'msgDirectionType', title: 'event.type'}, + {key: 'entityId', title: 'event.entity-id'}, + {key: 'entityName', title: 'event.entity-type'}, + {key: 'msgType', title: 'event.message-type'}, + {key: 'relationType', title: 'event.relation-type'}, + {key: 'dataSearch', title: 'event.data'}, + {key: 'metadataSearch', title: 'event.metadata'}, + {key: 'isError', title: 'event.error'}, + {key: 'error', title: 'event.error'} + ); + break; + } + } + + private editEventFilter($event: MouseEvent) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: EVENT_FILTER_PANEL_DATA, + useValue: { + columns: this.filterColumns, + filterParams: this.filterParams + } as EventFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) { + this.filterParams = componentRef.instance.result.filterParams; + this.table.updateData(); + } + }); + this.cd.detectChanges(); + } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index 6aaa256ab8..00aeb4f50c 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; @@ -24,6 +24,7 @@ import { EventTableConfig } from './event-table-config'; import { EventService } from '@core/http/event.service'; import { DialogService } from '@core/services/dialog.service'; import { DebugEventType, EventType } from '@shared/models/event.models'; +import { Overlay } from '@angular/cdk/overlay'; @Component({ selector: 'tb-event-table', @@ -80,7 +81,10 @@ export class EventTableComponent implements OnInit { private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, - private dialog: MatDialog) { + private dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { } ngOnInit() { @@ -95,7 +99,10 @@ export class EventTableComponent implements OnInit { this.tenantId, this.defaultEventType, this.disabledEventTypes, - this.debugEventTypes + this.debugEventTypes, + this.overlay, + this.viewContainerRef, + this.cd ); } diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index c6c8a5ed51..d5299a52ea 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -15,11 +15,21 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { BooleanFilterPredicate, BooleanOperation, - booleanOperationTranslationMap, EntityKeyValueType, + booleanOperationTranslationMap, + EntityKeyValueType, FilterPredicateType } from '@shared/models/query/query.models'; @@ -32,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BooleanFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BooleanFilterPredicateComponent), + multi: true } ] }) -export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); @@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On } } + validate(): ValidationErrors | null { + return this.booleanFilterPredicateFormGroup ? null : { + booleanFilterPredicate: {valid: false} + }; + } + writeValue(predicate: BooleanFilterPredicate): void { this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); } private updateModel() { - let predicate: BooleanFilterPredicate = null; - if (this.booleanFilterPredicateFormGroup.valid) { - predicate = this.booleanFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.BOOLEAN; - } + const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.BOOLEAN; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 9f4acfc3c6..b177f118c0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -16,11 +16,7 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { - ComplexFilterPredicate, - ComplexFilterPredicateInfo, - EntityKeyValueType -} from '@shared/models/query/query.models'; +import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { ComplexFilterPredicateDialogComponent, @@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 3c16b01319..75c9bd3ce8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -21,7 +21,10 @@ import { FormArray, FormBuilder, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { Observable, of, Subscription } from 'rxjs'; @@ -49,10 +52,15 @@ import { map } from 'rxjs/operators'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateListComponent), + multi: true } ] }) -export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni } } + validate(control: AbstractControl): ValidationErrors | null { + return this.filterListFormGroup.valid ? null : { + filterList: {valid: false} + }; + } + writeValue(predicates: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); @@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni private updateModel() { const predicates: Array = this.filterListFormGroup.getRawValue().predicates; - if (this.filterListFormGroup.valid && predicates.length) { + if (predicates.length) { this.propagateChange(predicates); } else { this.propagateChange(null); diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 7417317dae..934a4774fb 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -19,7 +19,10 @@ import { ControlValueAccessor, FormBuilder, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, ValidatorFn, Validators } from '@angular/forms'; @@ -39,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateValueComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateValueComponent), + multi: true } ] }) -export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { private readonly inheritModeForSources: DynamicValueSourceType[] = [ DynamicValueSourceType.CURRENT_CUSTOMER, @@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn } } - @Input() onlyUserDynamicSource = false; + private onlyUserDynamicSourceValue = false; + + @Input() + set onlyUserDynamicSource(dynamicMode: boolean) { + this.onlyUserDynamicSourceValue = dynamicMode; + if (this.filterPredicateValueFormGroup) { + this.updateValidationDynamicMode(); + setTimeout(() => { + this.updateModel(); + }, 0); + } + } + + get onlyUserDynamicSource(): boolean { + return this.onlyUserDynamicSourceValue; + } @Input() valueType: EntityKeyValueType; @@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn allow = true; private propagateChange = null; + private propagateChangePending = false; constructor(private fb: FormBuilder) { } @@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn this.updateShowInheritMode(sourceType); } ); + this.updateValidationDynamicMode(); this.filterPredicateValueFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); @@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn registerOnChange(fn: any): void { this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.updateModel(); + }, 0); + } } registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.filterPredicateValueFormGroup.disable({emitEvent: false}); @@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn } } + validate(): ValidationErrors | null { + return this.filterPredicateValueFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + writeValue(predicateValue: FilterPredicateValue): void { + this.propagateChangePending = false; this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.sourceType : null, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.inherit : false, {emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue').patchValue({ + sourceType: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceType : null, + sourceAttribute: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null, + inherit: predicateValue.dynamicValue ? predicateValue.dynamicValue.inherit : false + }, {emitEvent: this.onlyUserDynamicSource}); this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType); } private updateModel() { - let predicateValue: FilterPredicateValue = null; - if (this.filterPredicateValueFormGroup.valid) { - predicateValue = this.filterPredicateValueFormGroup.getRawValue(); - if (predicateValue.dynamicValue) { - if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) { - predicateValue.dynamicValue = null; - } + const predicateValue: FilterPredicateValue = this.filterPredicateValueFormGroup.getRawValue(); + if (predicateValue.dynamicValue) { + if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) { + predicateValue.dynamicValue = null; } } - this.propagateChange(predicateValue); + if (this.propagateChange) { + this.propagateChange(predicateValue); + } else { + this.propagateChangePending = true; + } } private updateShowInheritMode(sourceType: DynamicValueSourceType) { @@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn this.inheritMode = false; } } + + private updateValidationDynamicMode() { + if (this.onlyUserDynamicSource) { + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').setValidators(Validators.required); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').setValidators(Validators.required); + } else { + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').clearValidators(); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').clearValidators(); + } + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').updateValueAndValidity({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').updateValueAndValidity({emitEvent: false}); + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index 69972b6a90..17361f55bd 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -15,11 +15,17 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { - EntityKeyValueType, - FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo -} from '@shared/models/query/query.models'; + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; @Component({ selector: 'tb-filter-predicate', @@ -30,10 +36,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateComponent), + multi: true } ] }) -export class FilterPredicateComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.filterPredicateFormGroup.disable({emitEvent: false}); @@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { } } + validate(): ValidationErrors | null { + return this.filterPredicateFormGroup.valid ? null : { + filterPredicate: {valid: false} + }; + } + writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 1229f18ec2..c1d1a1db82 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -22,7 +22,10 @@ import { FormBuilder, FormControl, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; @@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => KeyFilterListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true } ] }) -export class KeyFilterListComponent implements ControlValueAccessor, OnInit { +export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } } + validate(): ValidationErrors | null { + return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : { + keyFilterList: {valid: false} + }; + } + writeValue(keyFilters: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index 434c90fa80..2b24a5a6b1 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -15,7 +15,16 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { EntityKeyValueType, FilterPredicateType, @@ -33,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumericFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NumericFilterPredicateComponent), + multi: true } ] }) -export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.numericFilterPredicateFormGroup.disable({emitEvent: false}); @@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On } } + validate(): ValidationErrors | null { + return this.numericFilterPredicateFormGroup.valid ? null : { + numericFilterPredicate: {valid: false} + }; + } + writeValue(predicate: NumericFilterPredicate): void { this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); } private updateModel() { - let predicate: NumericFilterPredicate = null; - if (this.numericFilterPredicateFormGroup.valid) { - predicate = this.numericFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.NUMERIC; - } + const predicate: NumericFilterPredicate = this.numericFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.NUMERIC; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index 11b4dad275..3caa282576 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -15,7 +15,16 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { EntityKeyValueType, FilterPredicateType, @@ -33,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StringFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => StringFilterPredicateComponent), + multi: true } ] }) -export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); } + validate(c): ValidationErrors { + return this.stringFilterPredicateFormGroup.valid ? null : { + stringFilterPredicate: {valid: false} + }; + } + private updateModel() { - let predicate: StringFilterPredicate = null; - if (this.stringFilterPredicateFormGroup.valid) { - predicate = this.stringFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.STRING; - } + const predicate: StringFilterPredicate = this.stringFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.STRING; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index ec90220d87..f2208757d4 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -25,6 +25,7 @@ import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableComponent } from '@home/components/event/event-table.component'; +import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component'; import { RelationTableComponent } from '@home/components/relation/relation-table.component'; import { RelationDialogComponent } from '@home/components/relation/relation-dialog.component'; import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component'; @@ -150,6 +151,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag EventContentDialogComponent, EventTableHeaderComponent, EventTableComponent, + EventFilterPanelComponent, EdgeDownlinkTableHeaderComponent, EdgeDownlinkTableComponent, RelationTableComponent, diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html index e04ecfb258..320373e52c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html @@ -61,6 +61,21 @@ {{ 'device-profile.attributes-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema-hint + + + device-profile.rpc-response-proto-schema + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts index 4a686d39bd..dc1da3d973 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts @@ -24,6 +24,8 @@ import { coapDeviceTypeTranslationMap, CoapTransportDeviceType, defaultAttributesSchema, + defaultRpcRequestSchema, + defaultRpcResponseSchema, defaultTelemetrySchema, DeviceProfileTransportConfiguration, DeviceTransportType, @@ -59,7 +61,9 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control private transportPayloadTypeConfiguration = this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], deviceTelemetryProtoSchema: [defaultTelemetrySchema, Validators.required], - deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required] + deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required], + deviceRpcRequestProtoSchema: [defaultRpcRequestSchema, Validators.required], + deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] }); get required(): boolean { 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 84797bcb98..5363d05d74 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 @@ -82,8 +82,6 @@ {{ 'device-profile.lwm2m.required' | translate }} -
-
{{ 'device-profile.lwm2m.default-min-period' | translate }} @@ -93,13 +91,16 @@ {{ 'device-profile.lwm2m.required' | translate }} - +
+
+ {{ 'device-profile.lwm2m.binding' | translate }} - - - {{ 'device-profile.lwm2m.binding' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - + + + {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 4231ab35ff..d6f6823db6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -14,15 +14,16 @@ /// limitations under the License. /// -import {DeviceProfileTransportConfiguration, DeviceTransportType} from '@shared/models/device.models'; -import {Component, forwardRef, Inject, Input} from '@angular/core'; -import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; -import {Store} from '@ngrx/store'; -import {AppState} from '@app/core/core.state'; -import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import { DeviceProfileTransportConfiguration } from '@shared/models/device.models'; +import { Component, forwardRef, Inject, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ATTRIBUTE, - DEFAULT_BINDING, + BINDING_MODE, + BINDING_MODE_NAMES, getDefaultProfileConfig, INSTANCES, KEY_NAME, @@ -34,11 +35,11 @@ import { RESOURCES, TELEMETRY } from './lwm2m-profile-config.models'; -import {DeviceProfileService} from '@core/http/device-profile.service'; -import {deepClone, isDefinedAndNotNull, isEmpty, isUndefined} from '@core/utils'; -import {WINDOW} from '@core/services/window.service'; -import {JsonArray, JsonObject} from '@angular/compiler-cli/ngcc/src/packages/entry_point'; -import {Direction} from '@shared/models/page/sort-order'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; +import { WINDOW } from '@core/services/window.service'; +import { JsonArray, JsonObject } from '@angular/compiler-cli/ngcc/src/packages/entry_point'; +import { Direction } from '@shared/models/page/sort-order'; @Component({ selector: 'tb-profile-lwm2m-device-transport-configuration', @@ -55,6 +56,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private requiredValue: boolean; private disabled = false; + bindingModeType = BINDING_MODE; + bindingModeTypes = Object.keys(BINDING_MODE); + bindingModeTypeNamesMap = BINDING_MODE_NAMES; lwm2mDeviceProfileFormGroup: FormGroup; lwm2mDeviceConfigFormGroup: FormGroup; bootstrapServers: string; @@ -86,7 +90,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro lifetime: [null, Validators.required], defaultMinPeriod: [null, Validators.required], notifIfDisabled: [true, []], - binding: [DEFAULT_BINDING, Validators.required], + binding:[], bootstrapServer: [null, Validators.required], lwm2mServer: [null, Validators.required], }); @@ -173,7 +177,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } private updateObserveAttrTelemetryObjectFormGroup = (objectsList: ObjectLwM2M[]): void => { - this.lwm2mDeviceProfileFormGroup.patchValue({ + this.lwm2mDeviceProfileFormGroup.patchValue({ observeAttrTelemetry: deepClone(this.getObserveAttrTelemetryObjects(objectsList)) }, {emitEvent: false}); @@ -358,6 +362,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro 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; } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html index 2f16e8677c..759e880ffa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -47,6 +47,7 @@
-
+
( + [ + [BINDING_MODE.U, 'U: UDP connection in standard mode'], + [BINDING_MODE.UQ, 'UQ: UDP connection in queue mode'], + [BINDING_MODE.US, 'US: both UDP and SMS connections active, both in standard mode'], + [BINDING_MODE.UQS, 'UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode'], + [BINDING_MODE.T,'T: TCP connection in standard mode'], + [BINDING_MODE.TQ, 'TQ: TCP connection in queue mode'], + [BINDING_MODE.TS, 'TS: both TCP and SMS connections active, both in standard mode'], + [BINDING_MODE.TQS, 'TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode'], + [BINDING_MODE.S, 'S: SMS connection in standard mode'], + [BINDING_MODE.SQ, 'SQ: SMS connection in queue mode'] + ] +); + export enum ATTRIBUTE_LWM2M_ENUM { dim = 'dim', ver = 'ver', @@ -62,8 +91,7 @@ export const ATTRIBUTE_LWM2M_LABEL = new Map( [ATTRIBUTE_LWM2M_ENUM.pmax, 'pmax='], [ATTRIBUTE_LWM2M_ENUM.gt, '>'], [ATTRIBUTE_LWM2M_ENUM.lt, '<'], - [ATTRIBUTE_LWM2M_ENUM.st, 'st='], - + [ATTRIBUTE_LWM2M_ENUM.st, 'st='] ] ); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 670e6fbb72..55f0869dbc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -88,6 +88,21 @@ {{ 'device-profile.attributes-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema-hint + + + device-profile.rpc-response-proto-schema + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index a61ad841aa..381e2f9e8d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -29,6 +29,8 @@ import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { defaultAttributesSchema, + defaultRpcRequestSchema, + defaultRpcResponseSchema, defaultTelemetrySchema, DeviceProfileTransportConfiguration, DeviceTransportType, @@ -90,7 +92,9 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control transportPayloadTypeConfiguration: this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], deviceTelemetryProtoSchema: [defaultTelemetrySchema, Validators.required], - deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required] + deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required], + deviceRpcRequestProtoSchema: [defaultRpcRequestSchema, Validators.required], + deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] }) }, {validator: this.uniqueDeviceTopicValidator} ); @@ -139,15 +143,21 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control if (forceUpdated) { transportPayloadTypeForm.patchValue({ deviceTelemetryProtoSchema: defaultTelemetrySchema, - deviceAttributesProtoSchema: defaultAttributesSchema + deviceAttributesProtoSchema: defaultAttributesSchema, + deviceRpcRequestProtoSchema: defaultRpcRequestSchema, + deviceRpcResponseProtoSchema: defaultRpcResponseSchema }, {emitEvent: false}); } if (type === TransportPayloadType.PROTOBUF && !this.disabled) { transportPayloadTypeForm.get('deviceTelemetryProtoSchema').enable({emitEvent: false}); transportPayloadTypeForm.get('deviceAttributesProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').enable({emitEvent: false}); } else { transportPayloadTypeForm.get('deviceTelemetryProtoSchema').disable({emitEvent: false}); transportPayloadTypeForm.get('deviceAttributesProtoSchema').disable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').disable({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html new file mode 100644 index 0000000000..81c06fe0f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -0,0 +1,69 @@ + + +
+
+
+
+ +
+
+ + +
+
+ +
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.no-datakey-selected' | translate }} +
+
+ {{ errorMessage | translate }} +
+
+
+
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss similarity index 70% rename from dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java rename to ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss index 2af4881c84..f097de5afb 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss @@ -13,11 +13,24 @@ * 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.BaseTbResourceServiceTest; -import org.thingsboard.server.dao.service.DaoSqlTest; +.tb-json-input { + width: 100%; + height: 100%; + padding: 5px; -@DaoSqlTest -public class TbResourceServiceSqlTest extends BaseTbResourceServiceTest { + &__form { + overflow: auto; + height: 100%; + } + + &__error { + text-align: center; + font-size: 18px; + color: #a0a0a0; + } +} + +.tb-toast { + font-size: 14px!important; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts new file mode 100644 index 0000000000..ce174fcde0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.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, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { createLabelFromDatasource } from '@core/utils'; +import { Observable } from 'rxjs'; + +enum JsonInputWidgetMode { + ATTRIBUTE = 'ATTRIBUTE', + TIME_SERIES = 'TIME_SERIES', +} + +interface JsonInputWidgetSettings { + widgetTitle: string; + widgetMode: JsonInputWidgetMode; + attributeScope?: AttributeScope; + showLabel: boolean; + labelValue?: string; + attributeRequired: boolean; + showResultMessage: boolean; +} + +@Component({ + selector: 'tb-json-input-widget ', + templateUrl: './json-input-widget.component.html', + styleUrls: ['./json-input-widget.component.scss'] +}) +export class JsonInputWidgetComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + public settings: JsonInputWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasource: Datasource; + + labelValue: string; + + entityDetected = false; + dataKeyDetected = false; + isValidParameter = false; + errorMessage: string; + + isFocused: boolean; + originalValue: any; + attributeUpdateFormGroup: FormGroup; + + toastTargetId = 'json-input-widget' + this.utils.guid(); + + constructor(protected store: Store, + private utils: UtilsService, + private fb: FormBuilder, + private attributeService: AttributeService, + private translate: TranslateService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.jsonInputWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasource = this.subscription.datasources[0]; + this.initializeConfig(); + this.validateDatasources(); + this.buildForm(); + this.ctx.updateWidgetParams(); + } + + private initializeConfig() { + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { + const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); + this.ctx.widgetTitle = this.utils.customTranslation(title, title); + } else { + this.ctx.widgetTitle = this.ctx.widgetConfig.title; + } + + if (this.settings.labelValue && this.settings.labelValue.length) { + const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); + this.labelValue = this.utils.customTranslation(label, label); + } else { + this.labelValue = this.translate.instant('widgets.input-widgets.value'); + } + } + + private validateDatasources() { + if (this.datasource?.type === DatasourceType.entity) { + this.entityDetected = true; + if (this.datasource.dataKeys.length) { + this.dataKeyDetected = true; + + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { + if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) { + this.isValidParameter = true; + } else { + this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; + } + } else { + this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; + } + } else { + if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { + this.isValidParameter = true; + } else { + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; + } + } + + } + } + } + + private buildForm() { + const validators: ValidatorFn[] = []; + if (this.settings.attributeRequired) { + validators.push(Validators.required); + } + this.attributeUpdateFormGroup = this.fb.group({ + currentValue: [{}, validators] + }); + this.attributeUpdateFormGroup.valueChanges.subscribe(() => { + this.ctx.detectChanges(); + }); + } + + private updateWidgetData(data: Array) { + if (this.isValidParameter) { + let value = {}; + if (data[0].data[0][1] !== '') { + try { + value = JSON.parse(data[0].data[0][1]); + } catch (e) { + value = data[0].data[0][1]; + } + } + this.originalValue = value; + if (!this.isFocused) { + this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue); + this.ctx.detectChanges(); + } + } + } + + public onDataUpdated() { + this.updateWidgetData(this.subscription.data); + } + + public save() { + this.isFocused = false; + + const attributeToSave: AttributeData = { + key: this.datasource.dataKeys[0].name, + value: this.attributeUpdateFormGroup.get('currentValue').value + }; + + const entityId: EntityId = { + entityType: this.datasource.entityType, + id: this.datasource.entityId + }; + + let saveAttributeObservable: Observable; + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { + saveAttributeObservable = this.attributeService.saveEntityAttributes( + entityId, + this.settings.attributeScope, + [attributeToSave], + {} + ); + } else { + saveAttributeObservable = this.attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + [attributeToSave], + {} + ); + } + saveAttributeObservable.subscribe( + () => { + this.attributeUpdateFormGroup.markAsPristine(); + this.ctx.detectChanges(); + if (this.settings.showResultMessage) { + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), + 1000, 'bottom', 'left', this.toastTargetId); + } + }, + () => { + if (this.settings.showResultMessage) { + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), + 'bottom', 'left', this.toastTargetId); + } + }); + } + + public discard() { + this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false}); + this.attributeUpdateFormGroup.markAsPristine(); + this.isFocused = false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index 077d8dfb6d..32a9074ca0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -19,7 +19,8 @@ import { createLabelFromDatasource, hashCode, isDefined, - isDefinedAndNotNull, isFunction, + isDefinedAndNotNull, + isFunction, isNumber, isUndefined, padValue @@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models'; import _ from 'lodash'; import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; import { addCondition, mergeSchemes } from '@core/schema-utils'; -import L, {Projection} from "leaflet"; +import L from 'leaflet'; export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { const providerSchema = _.cloneDeep(mapProviderSchema); @@ -318,22 +319,24 @@ export const parseWithTranslation = { }; export function parseData(input: DatasourceData[]): FormattedData[] { - return _(input).groupBy(el => el?.datasource?.entityName) + return _(input).groupBy(el => el?.datasource.entityId + el?.datasource.entityType) .values().value().map((entityArray, i) => { const obj: FormattedData = { entityName: entityArray[0]?.datasource?.entityName, - entityId: entityArray[0]?.datasource?.entityId, - entityType: entityArray[0]?.datasource?.entityType, - $datasource: entityArray[0]?.datasource, + entityId: entityArray[0].datasource.entityId, + entityType: entityArray[0].datasource.entityType, + $datasource: entityArray[0].datasource, dsIndex: i, deviceType: null }; entityArray.filter(el => el.data.length).forEach(el => { - const indexDate = el?.data?.length ? el.data.length - 1 : 0; - obj[el?.dataKey?.label] = el?.data[indexDate][1]; - obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; - if (el?.dataKey?.label === 'type') { - obj.deviceType = el?.data[indexDate][1]; + const indexDate = el.data.length ? el.data.length - 1 : 0; + if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') { + obj[el.dataKey.label] = el.data[indexDate][1]; + obj[el.dataKey.label + '|ts'] = el.data[indexDate][0]; + if (el.dataKey.label === 'type') { + obj.deviceType = el.data[indexDate][1]; + } } }); return obj; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index b1b5a3b336..d2cf53265f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy this.switchResize$ = new ResizeObserver(() => { this.resize(); - }) + }); this.switchResize$.observe(this.switchContainerRef.nativeElement); this.init(); } @@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy let width = this.switchContainer.width(); let height = this.switchContainer.height(); if (this.showOnOffLabels) { - height = height*2/3; + height = height * 2 / 3; } - const ratio = width/height; + const ratio = width / height; if (ratio > switchAspectRation) { - width = height*switchAspectRation; + width = height * switchAspectRation; } else { - height = width/switchAspectRation; + height = width / switchAspectRation; } this.switchElement.css({width, height}); this.matSlideToggle.css({width, height, minWidth: width}); @@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy fontSize--; textWidth = this.measureTextWidth(text, fontSize); } - element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + element.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'}); } private measureTextWidth(text: string, fontSize: number): number { - this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'}); this.textMeasure.text(text); return this.textMeasure.width(); } @@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( (responseBody) => { this.setValue(this.parseValueFunction(responseBody)); + this.ctx.detectChanges(); }, () => { const errorText = this.ctx.defaultSubscription.rpcErrorText; 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 2ad0fb1612..b55c21b179 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 @@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor 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'; +import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; @NgModule({ declarations: @@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- EdgesOverviewWidgetComponent, DateRangeNavigatorWidgetComponent, DateRangeNavigatorPanelComponent, + JsonInputWidgetComponent, MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, @@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- EdgesOverviewWidgetComponent, RpcWidgetsModule, DateRangeNavigatorWidgetComponent, + JsonInputWidgetComponent, MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, @@ -84,4 +87,5 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- ImportExportService ] }) -export class WidgetComponentsModule { } +export class WidgetComponentsModule { +} diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw index b54ce8ea99..0a037e80bb 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw @@ -4413,7 +4413,8 @@ "showDashboardExport": false, "toolbarAlwaysOpen": true, "titleColor": "rgba(0,0,0,0.870588)", - "showFilters": false + "showFilters": false, + "showDashboardLogo": false } }, "name": "Api Usage" diff --git a/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts index 57f4a18205..ae24710467 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts @@ -89,7 +89,7 @@ export class AssetsTableConfigResolver implements Resolve this.translate.instant('asset.delete-asset-title', { assetName: asset.name }); + this.config.deleteEntityTitle = asset => this.translate.instant('asset.delete-asset-title', {assetName: asset.name}); this.config.deleteEntityContent = () => this.translate.instant('asset.delete-asset-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('asset.delete-assets-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('asset.delete-assets-text'); @@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve this.onAssetAction(action); - this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user'; + this.config.detailsReadonly = () => (this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user'); this.config.headerComponent = AssetTableHeaderComponent; @@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve this.config.componentsData.assetScope === 'tenant'; return this.config; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index 61422f1fa3..c572fa958f 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -93,7 +93,7 @@ export class DashboardsTableConfigResolver implements Resolve - this.translate.instant('dashboard.delete-dashboard-title', { dashboardTitle: dashboard.title }); + this.translate.instant('dashboard.delete-dashboard-title', {dashboardTitle: dashboard.title}); this.config.deleteEntityContent = () => this.translate.instant('dashboard.delete-dashboard-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('dashboard.delete-dashboards-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('dashboard.delete-dashboards-text'); @@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve this.onDashboardAction(action); - this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user'; + this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user'); } resolve(route: ActivatedRouteSnapshot): Observable> { @@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve this.config.componentsData.dashboardScope === 'tenant'; return this.config; @@ -363,8 +363,7 @@ export class DashboardsTableConfigResolver implements Resolve { - this.config.table.updateData(); + this.config.table.updateData(); }); } ); diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 1349ffae1e..0a6c4e9610 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -97,7 +97,7 @@ export class DevicesTableConfigResolver implements Resolve this.translate.instant('device.delete-device-title', { deviceName: device.name }); + this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', {deviceName: device.name}); this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text'); @@ -109,10 +109,10 @@ export class DevicesTableConfigResolver implements Resolve this.deviceService.getDeviceInfo(savedDevice.id.id) - )); + )); }; this.config.onEntityAction = action => this.onDeviceAction(action); - this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user'; + this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'); this.config.headerComponent = DeviceTableHeaderComponent; @@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve this.config.componentsData.deviceScope === 'tenant'; return this.config; @@ -208,7 +208,7 @@ export class DevicesTableConfigResolver implements Resolve this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink, this.config.componentsData.deviceProfileId !== null ? - this.config.componentsData.deviceProfileId.id : ''); + this.config.componentsData.deviceProfileId.id : ''); this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); } } @@ -250,26 +250,26 @@ export class DevicesTableConfigResolver implements Resolve (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), - onAction: ($event, entity) => this.unassignFromCustomer($event, entity) - }, - { - name: this.translate.instant('device.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('device.manage-credentials'), - icon: 'security', - isEnabled: () => true, - onAction: ($event, entity) => this.manageCredentials($event, entity) - } - ); + actions.push( + { + name: this.translate.instant('device.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('device.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('device.manage-credentials'), + icon: 'security', + isEnabled: () => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); } if (deviceScope === 'customer_user' || deviceScope === 'edge_customer_user') { actions.push( @@ -452,10 +452,10 @@ export class DevicesTableConfigResolver implements Resolve { - if (res) { - this.config.table.updateData(); - } - }); + if (res) { + this.config.table.updateData(); + } + }); } unassignFromCustomer($event: Event, device: DeviceInfo) { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index da5aedf53b..29f5ab3579 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -33,8 +33,11 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; import { importRuleChainBreadcumbLabelFunction, ResolvedRuleChainMetaDataResolver, - ruleChainBreadcumbLabelFunction, RuleChainImportGuard, - RuleChainResolver, RuleNodeComponentsResolver, TooltipsterResolver + ruleChainBreadcumbLabelFunction, + RuleChainImportGuard, + RuleChainResolver, + RuleNodeComponentsResolver, + TooltipsterResolver } from '@home/pages/rulechain/rulechain-routing.module'; const routes: Routes = [ @@ -58,21 +61,6 @@ const routes: Routes = [ entitiesTableConfig: EdgesTableConfigResolver } }, - { - path: ':edgeId/ruleChains', - component: EntitiesTableComponent, - data: { - auth: [Authority.TENANT_ADMIN], - ruleChainsType: 'edge', - breadcrumb: { - label: 'edge.edge-rulechains', - icon: 'settings_ethernet' - }, - }, - resolve: { - entitiesTableConfig: RuleChainsTableConfigResolver - } - }, { path: ':edgeId/assets', component: EntitiesTableComponent, @@ -156,6 +144,50 @@ const routes: Routes = [ } ] }, + { + path: ':edgeId/ruleChains', + data: { + breadcrumb: { + label: 'edge.edge-rulechains', + icon: 'settings_ethernet' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'edge.rulechains', + ruleChainsType: 'edge' + }, + resolve: { + entitiesTableConfig: RuleChainsTableConfigResolver + } + }, + { + path: ':ruleChainId', + component: RuleChainPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: ruleChainBreadcumbLabelFunction, + icon: 'settings_ethernet' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN], + title: 'rulechain.edge-rulechain', + import: false, + ruleChainType: RuleChainType.EDGE + }, + resolve: { + ruleChain: RuleChainResolver, + ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleNodeComponents: RuleNodeComponentsResolver, + tooltipster: TooltipsterResolver + } + } + ] + }, { path: 'ruleChains', data: { @@ -220,7 +252,7 @@ const routes: Routes = [ } ] } - ] + ] }]; @NgModule({ @@ -230,4 +262,5 @@ const routes: Routes = [ EdgesTableConfigResolver ] }) -export class EdgeRoutingModule { } +export class EdgeRoutingModule { +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 3177c5194f..6ae548acb2 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -44,31 +44,31 @@
@@ -85,7 +85,7 @@ ngxClipboard (cbOnSuccess)="onEdgeInfoCopied('key')" [cbContent]="entity?.routingKey" - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> + [fxShow]="!isEdit && edgeScope !== 'customer_user'"> edge.copy-edge-key @@ -93,7 +93,7 @@ ngxClipboard (cbOnSuccess)="onEdgeInfoCopied('secret')" [cbContent]="entity?.secret" - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> + [fxShow]="!isEdit && edgeScope !== 'customer_user'"> edge.copy-edge-secret @@ -101,7 +101,7 @@ ngxClipboard [disabled]="(isLoading$ | async)" (click)="onEntityAction($event, 'syncEdge')" - [fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> + [fxShow]="!isEdit && edgeScope !== 'customer_user'"> edge.sync @@ -132,7 +132,7 @@ [required]="true" [entityType]="entityType.EDGE"> -
+
edge.edge-license-key @@ -142,7 +142,7 @@
-
+
edge.cloud-endpoint-hint
edge.cloud-endpoint @@ -153,7 +153,7 @@
-
+
edge.edge-key @@ -164,7 +164,7 @@
-
+
edge.edge-secret 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 index 96251948d5..6cbeff0efe 100644 --- 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 @@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve - this.translate.instant('entity-view.delete-entity-view-title', { entityViewName: entityView.name }); + this.translate.instant('entity-view.delete-entity-view-title', {entityViewName: entityView.name}); this.config.deleteEntityContent = () => this.translate.instant('entity-view.delete-entity-view-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('entity-view.delete-entity-views-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('entity-view.delete-entity-views-text'); @@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve this.onEntityViewAction(action); - this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user'; + this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user'); this.config.headerComponent = EntityViewTableHeaderComponent; @@ -143,8 +143,7 @@ export class EntityViewsTableConfigResolver implements Resolve this.config.tableTitle = edge.name + ': ' + this.translate.instant('entity-view.entity-views') ); - } - else { + } else { this.config.tableTitle = this.translate.instant('entity-view.entity-views'); } this.config.columns = this.configureColumns(this.config.componentsData.entityViewScope); @@ -152,7 +151,7 @@ export class EntityViewsTableConfigResolver implements Resolve this.config.componentsData.entityViewScope === 'tenant'; return this.config; diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index eaca7133a0..f1a42344fb 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -57,13 +57,13 @@ language.language - + {{ lang ? ('language.locales.' + lang | translate) : ''}} -
+
, private route: ActivatedRoute, @@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir public dialogService: DialogService, public fb: FormBuilder) { super(store); + this.authUser = getCurrentAuthUser(this.store); } ngOnInit() { @@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir return this.profile; } + isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + } diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts index 55dc474a3d..d6759310ac 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts @@ -24,7 +24,6 @@ import { import { Resolve } from '@angular/router'; import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { Direction } from '@shared/models/page/sort-order'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DatePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; @@ -34,13 +33,14 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component'; -import { Observable } from 'rxjs/internal/Observable'; -import { PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { map } from 'rxjs/operators'; @Injectable() -export class ResourcesLibraryTableConfigResolver implements Resolve> { +export class ResourcesLibraryTableConfigResolver implements Resolve> { - private readonly config: EntityTableConfig = new EntityTableConfig(); + private readonly config: EntityTableConfig = new EntityTableConfig(); private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(private store: Store, @@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve resource ? resource.title : ''; this.config.columns.push( new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('title', 'widgets-bundle.title', '60%'), + new EntityTableColumn('title', 'resource.title', '60%'), new EntityTableColumn('resourceType', 'resource.resource-type', '40%', entity => this.resourceTypesTranslationMap.get(entity.resourceType)), - new EntityTableColumn('tenantId', 'widgets-bundle.system', '60px', + new EntityTableColumn('tenantId', 'resource.system', '60px', entity => { return checkBoxCell(entity.tenantId.id === NULL_UUID); }), @@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve this.translate.instant('resource.delete-resources-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); - this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable>; + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink); this.config.loadEntity = id => this.resourceService.getResource(id.id); - this.config.saveEntity = resource => this.resourceService.saveResource(resource); + this.config.saveEntity = resource => this.saveResource(resource); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); + + this.config.onEntityAction = action => this.onResourceAction(action); + } + + saveResource(resource) { + if (Array.isArray(resource.data)) { + const resources = []; + resource.data.forEach((data, index) => { + resources.push({ + resourceType: resource.resourceType, + data, + fileName: resource.fileName[index], + title: resource.title + }); + }); + return this.resourceService.saveResources(resources, {resendRequest: true}).pipe( + map((response) => response[0]) + ); + } else { + return this.resourceService.saveResource(resource); + } } - resolve(): EntityTableConfig { + resolve(): EntityTableConfig { this.config.tableTitle = this.translate.instant('resource.resources-library'); const authUser = getCurrentAuthUser(this.store); this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); @@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve): boolean { + switch (action.action) { + case 'uploadResource': + this.exportResource(action.event, action.entity); + return true; + } + return false; + } + + private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean { if (authority === Authority.TENANT_ADMIN) { return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; } else { diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html index e4fa600f9c..5b98fa9c61 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html @@ -16,6 +16,12 @@ -->
+ -
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 2b9572b7fa..37a21cab2e 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 @@ -73,7 +73,7 @@ export class RuleChainsTableConfigResolver implements Resolve this.translate.instant('rulechain.delete-rulechain-title', - { ruleChainName: ruleChain.name }); + {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'); @@ -121,11 +121,11 @@ export class RuleChainsTableConfigResolver implements Resolve('root', 'rulechain.root', '60px', entity => { - if (ruleChainScope === 'edge') { - return checkBoxCell((this.config.componentsData.edge.rootRuleChainId.id === entity.id.id)); - } else { - return checkBoxCell(entity.root); - } + if (ruleChainScope === 'edge') { + return checkBoxCell((this.config.componentsData.edge.rootRuleChainId.id === entity.id.id)); + } else { + return checkBoxCell(entity.root); + } }) ); } else if (ruleChainScope === 'edges') { @@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve> { 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.open-rulechain'), - icon: 'settings_ethernet', - isEnabled: () => true, - onAction: ($event, entity) => this.openRuleChain($event, entity) + 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.export'), - icon: 'file_download', - isEnabled: () => true, - onAction: ($event, entity) => this.exportRuleChain($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 === '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( @@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve { - 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(); + 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); } - ); - } else { - this.config.table.updateData(); + 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) { @@ -510,13 +510,13 @@ export class RuleChainsTableConfigResolver implements Resolve { - if (res) { - this.ruleChainService.setAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( - () => { - this.config.table.updateData(); - } - ); - } + if (res) { + this.ruleChainService.setAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } } ); } diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index ea0ae424bb..5f3263522c 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -18,7 +18,7 @@
+ [flowConfig]="{allowDuplicateUploads: true}">
diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index b91fba1def..09f9c1b17c 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -17,6 +17,7 @@ import { AfterViewInit, Component, + ElementRef, EventEmitter, forwardRef, Input, @@ -72,9 +73,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, contentConvertFunction: (content: string) => any; private requiredValue: boolean; + get required(): boolean { return this.requiredValue; } + @Input() set required(value: boolean) { const newVal = coerceBooleanProperty(value); @@ -84,9 +87,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } private requiredAsErrorValue: boolean; + get requiredAsError(): boolean { return this.requiredAsErrorValue; } + @Input() set requiredAsError(value: boolean) { const newVal = coerceBooleanProperty(value); @@ -102,17 +107,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, existingFileName: string; @Input() - convertToBase64 = false; + readAsBinary = false; + + private multipleFileValue = false; + + @Input() + set multipleFile(value: boolean) { + this.multipleFileValue = value; + if (this.flow?.flowJs) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + get multipleFile(): boolean { + return this.multipleFileValue; + } @Output() - fileNameChanged = new EventEmitter(); + fileNameChanged = new EventEmitter(); - fileName: string; + fileName: string | string[]; fileContent: any; @ViewChild('flow', {static: true}) flow: FlowDirective; + @ViewChild('flowInput', {static: true}) + flowInput: ElementRef; + autoUploadSubscription: Subscription; private propagateChange = null; @@ -125,34 +147,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, ngAfterViewInit() { this.autoUploadSubscription = this.flow.events$.subscribe(event => { - if (event.type === 'fileAdded') { - const file = event.event[0] as flowjs.FlowFile; - if (this.filterFile(file)) { - const reader = new FileReader(); - reader.onload = (loadEvent) => { - if (typeof reader.result === 'string') { - const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; - if (fileContent && fileContent.length > 0) { - if (this.contentConvertFunction) { - this.fileContent = this.contentConvertFunction(fileContent); - } else { - this.fileContent = fileContent; - } - if (this.fileContent) { - this.fileName = file.name; - } else { - this.fileName = null; - } - this.updateModel(); - } + if (event.type === 'filesAdded') { + const readers = []; + (event.event[0] as flowjs.FlowFile[]).forEach(file => { + if (this.filterFile(file)) { + readers.push(this.readerAsFile(file)); + } + }); + if (readers.length) { + Promise.all(readers).then((filesContent) => { + filesContent = filesContent.filter(content => content.fileContent != null); + if (filesContent.length === 1) { + this.fileContent = filesContent[0].fileContent; + this.fileName = filesContent[0].fileName; + this.updateModel(); + } else if (filesContent.length > 1) { + this.fileContent = filesContent.map(content => content.fileContent); + this.fileName = filesContent.map(content => content.fileName); + this.updateModel(); + } + }); + } + } + }); + if (!this.multipleFile) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + private readerAsFile(file: flowjs.FlowFile): Promise { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + let fileName = null; + let fileContent = null; + if (typeof reader.result === 'string') { + fileContent = reader.result; + if (fileContent && fileContent.length > 0) { + if (this.contentConvertFunction) { + fileContent = this.contentConvertFunction(fileContent); + } + if (fileContent) { + fileName = file.name; } - }; - if (this.convertToBase64) { - reader.readAsBinaryString(file.file); - } else { - reader.readAsText(file.file); } } + resolve({fileContent, fileName}); + }; + reader.onerror = () => { + resolve({fileContent: null, fileName: null}); + }; + if (this.readAsBinary) { + reader.readAsBinaryString(file.file); + } else { + reader.readAsText(file.file); } }); } @@ -207,4 +255,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.fileContent = null; this.updateModel(); } + + private updateMultipleFileMode(multiple: boolean) { + this.flow.flowJs.opts.singleFile = !multiple; + if (!multiple) { + this.flowInput.nativeElement.removeAttribute('multiple'); + } + } } diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index a5d45d16c7..78652faa18 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; -import { guid, isUndefined } from '@core/utils'; +import { guid, isDefinedAndNotNull, isLiteralObject, isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { getAce } from '@shared/models/ace/ace.models'; @@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va this.contentValue = ''; this.objectValid = false; try { - - if (this.modelValue) { + if (isDefinedAndNotNull(this.modelValue)) { this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : (key, objectValue) => { return this.sort(key, objectValue); @@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va if (this.contentValue && this.contentValue.length > 0) { try { data = JSON.parse(this.contentValue); + if (!isLiteralObject(data)) { + throw new TypeError(`Value is not a valid JSON`); + } this.objectValid = true; this.validationError = ''; } catch (ex) { diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.html b/ui-ngx/src/app/shared/components/time/datetime-period.component.html index 96b2e3d526..96bffe8696 100644 --- a/ui-ngx/src/app/shared/components/time/datetime-period.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.html @@ -15,8 +15,9 @@ limitations under the License. --> -
-
+
+
datetime.date-from @@ -30,7 +31,8 @@
-
+
datetime.date-to diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts index 3eab8c428d..3f9c712556 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts @@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode @Component({ selector: 'tb-quick-time-interval', templateUrl: './quick-time-interval.component.html', - styleUrls: ['./quick-time-interval.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 57c596bbc2..f947c51d3e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -51,7 +51,7 @@ [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" [required]="timewindow.selectedTab === timewindowTypes.REALTIME && timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" - style="padding-top: 8px; min-width: 364px"> + style="padding-top: 8px">
@@ -75,6 +75,7 @@ timewindow.time-period timewindow.interval + style="padding-top: 8px">
@@ -134,21 +137,23 @@ (ngModelChange)="onHideAggIntervalChanged()">
-
- aggregation.limit - - - - - +
+ +
+ + + + + +
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss index 6b25d0b1a7..fd8cb0f331 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../scss/constants"; + :host { width: 100%; height: 100%; @@ -40,21 +42,29 @@ } .limit-slider-container { - >:first-child { - margin-right: 16px; - } - >:last-child { + .limit-slider-value { margin-left: 16px; - } - >:first-child, >:last-child { min-width: 25px; - max-width: 42px; + max-width: 80px; } mat-form-field input[type=number] { text-align: center; } } + @media #{$mat-gt-sm} { + .history-time-input { + min-width: 364px; + } + .limit-slider-container { + > label { + margin-right: 16px; + width: min-content; + max-width: 40%; + } + } + } + } :host ::ng-deep { diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index a92bc3d672..6a90101923 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -86,6 +86,8 @@ export interface DashboardSettings { showDashboardsSelect?: boolean; showEntitiesSelect?: boolean; showFilters?: boolean; + showDashboardLogo?: boolean; + dashboardLogoUrl?: string; showDashboardTimewindow?: boolean; showDashboardExport?: boolean; toolbarAlwaysOpen?: boolean; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 96a2a6bc00..f3524916b4 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -151,6 +151,24 @@ export const defaultAttributesSchema = ' string serialNumber = 2;\n' + '}'; +export const defaultRpcRequestSchema = + 'syntax ="proto3";\n' + + 'package rpc;\n' + + '\n' + + 'message RpcRequestMsg {\n' + + ' string method = 1;\n' + + ' int32 requestId = 2;\n' + + ' string params = 3;\n' + + '}'; + +export const defaultRpcResponseSchema = + 'syntax ="proto3";\n' + + 'package rpc;\n' + + '\n' + + 'message RpcResponseMsg {\n' + + ' string payload = 1;\n' + + '}'; + export const coapDeviceTypeTranslationMap = new Map( [ [CoapTransportDeviceType.DEFAULT, 'device-profile.coap-device-type-default'], @@ -182,6 +200,13 @@ export const deviceTransportTypeConfigurationInfoMap = new Map { uid: string; body: EventBody; } + +export interface BaseFilterEventBody { + server?: string; +} + +export interface ErrorFilterEventBody extends BaseFilterEventBody { + method?: string; +} + +export interface LcFilterEventEventBody extends BaseFilterEventBody { + method?: string; + status?: string; + isError?: boolean; +} + +export interface StatsFilterEventBody extends BaseFilterEventBody { + messagesProcessed?: string; + errorsOccurred?: string; +} + +export interface DebugFilterRuleNodeEventBody extends BaseFilterEventBody { + msgDirectionType?: string; + entityId?: string; + entityName?: EntityType; + msgId?: string; + msgType?: string; + relationType?: string; + dataSearch?: string; + metadataSearch?: string; + isError?: boolean; +} + +export type FilterEventBody = ErrorFilterEventBody & LcFilterEventEventBody & StatsFilterEventBody & DebugFilterRuleNodeEventBody; diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 805a046673..082d09ac24 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -45,7 +45,7 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.LWM2M_MODEL, 'LWM2M model'], [ResourceType.PKCS_12, 'PKCS #12'], [ResourceType.JKS, 'JKS'] - ] + ] ); export interface ResourceInfo extends BaseData { @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo { data: string; fileName: string; } + +export interface Resources extends ResourceInfo { + data: string|string[]; + fileName: string|string[]; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 02cd3b93d4..15d36d6753 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; @NgModule({ providers: [ @@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i { provide: FlowInjectionToken, useValue: Flow + }, + { + provide: MAT_DATE_LOCALE, + useValue: 'en-GB' } ], declarations: [ diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 888154e3bf..92941c9404 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -1332,7 +1332,6 @@ "body": "Tělo", "method": "Metoda", "type": "Typ", - "entity": "Entita", "message-id": "Id zprávy", "message-type": "Typ zprávy", "data-type": "Typ dat", @@ -1344,7 +1343,9 @@ "success": "Úspěch", "failed": "Neúspěch", "messages-processed": "Zpracované zprávy", - "errors-occurred": "Vyskytly se chyby" + "errors-occurred": "Vyskytly se chyby", + "all-events": "Vše", + "entity-type": "Typ entity" }, "extension": { "extensions": "Rozšíření", diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index 712ad0fa2d..3dcf03e1b0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -1094,7 +1094,6 @@ "body": "Inhalt", "method": "Methode", "type": "Typ", - "entity": "Entität", "message-id": "Nachrichten-Id", "message-type": "Nachrichten-Typ", "data-type": "Datentyp", @@ -1106,7 +1105,9 @@ "success": "Erfolg", "failed": "Fehlgeschlagen", "messages-processed": "Nachrichten verarbeitet", - "errors-occurred": "Fehler aufgetreten" + "errors-occurred": "Fehler aufgetreten", + "all-events": "Alle", + "entity-type": "Entitätstyp" }, "extension": { "extensions": "Erweiterungen", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 2d2ab96de5..d92b6e7884 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -1247,7 +1247,6 @@ "out": "Έξοδος", "metadata": "Μεταδεδομένα", "message": "Μήνυμα", - "entity": "Οντότητα", "message-id": "ID Μηνύματος", "message-type": "Τύπος Μηνύματος", "data-type": "Τύπος Δεδομένων", @@ -1258,7 +1257,9 @@ "success": "Επιτυχία", "failed": "Απέτυχε", "messages-processed": "Επεξεργασμένα μηνύματα", - "errors-occurred": "Παρουσιάστηκαν σφάλματα" + "errors-occurred": "Παρουσιάστηκαν σφάλματα", + "all-events": "Όλοι", + "entity-type": "Τύπος οντοτήτων" }, "extension": { "extensions": "Επεκτάσεις", 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 fa18da65d4..41cff4e73c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -746,6 +746,8 @@ "display-filters": "Display filters", "display-dashboard-timewindow": "Display timewindow", "display-dashboard-export": "Display export", + "display-dashboard-logo": "Display logo in dashboard fullscreen mode", + "dashboard-logo-image": "Dashboard logo image", "import": "Import dashboard", "export": "Export dashboard", "export-failed-error": "Unable to export dashboard: {{error}}", @@ -1072,8 +1074,13 @@ "telemetry-proto-schema-required": "Telemetry proto schema is required.", "attributes-proto-schema": "Attributes proto schema", "attributes-proto-schema-required": "Attributes proto schema is required.", + "rpc-response-proto-schema": "RPC response proto schema", + "rpc-response-proto-schema-required": "RPC response proto schema is required.", "rpc-response-topic-filter": "RPC response topic filter", "rpc-response-topic-filter-required": "RPC response topic filter is required.", + "rpc-request-proto-schema": "RPC request proto schema", + "rpc-request-proto-schema-required": "RPC request proto schema is required.", + "rpc-request-proto-schema-hint": "RPC request message should have always fields: string method = 1; int32 requestId = 2; and params = 3 of any data type.", "not-valid-pattern-topic-filter": "Not valid pattern topic filter", "not-valid-single-character": "Invalid use of a single-level wildcard character", "not-valid-multi-character": "Invalid use of a multi-level wildcard character", @@ -1627,6 +1634,7 @@ }, "event": { "event-type": "Event type", + "events-filter": "Events Filter", "type-error": "Error", "type-lc-event": "Lifecycle event", "type-stats": "Statistics", @@ -1640,7 +1648,6 @@ "body": "Body", "method": "Method", "type": "Type", - "entity": "Entity", "message-id": "Message Id", "message-type": "Message Type", "data-type": "Data Type", @@ -1652,7 +1659,14 @@ "success": "Success", "failed": "Failed", "messages-processed": "Messages processed", - "errors-occurred": "Errors occurred" + "min-messages-processed": "Minimum messages processed", + "errors-occurred": "Errors occurred", + "min-errors-occurred": "Minimum errors occurred", + "min-value": "Minimum value is 0.", + "all-events": "All", + "has-error": "Has error", + "entity-id": "Entity Id", + "entity-type": "Entity type" }, "extension": { "extensions": "Extensions", 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 ba278e796e..9d18102a43 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -1521,7 +1521,6 @@ "body": "Cuerpo", "method": "Método", "type": "Tipo", - "entity": "Entidad", "message-id": "Id Mensaje", "message-type": "Tipo Mensaje", "data-type": "Tipo de Datos", @@ -1533,7 +1532,9 @@ "success": "Éxito", "failed": "Fallo", "messages-processed": "Mensajes procesados", - "errors-occurred": "Ocurrieron errores" + "errors-occurred": "Ocurrieron errores", + "all-events": "Todos", + "entity-type": "Tipo de entidad" }, "extension": { "extensions": "Extensiones", diff --git a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json index 5327f0b33b..90a10f765c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json @@ -881,7 +881,6 @@ "body": "بدنه", "method": "روش", "type": "نوع", - "entity": "موجودي", "message-id": "پيام ID", "message-type": "نوع پيام", "data-type": "نوع داده", @@ -893,7 +892,9 @@ "success": "موفقيت", "failed": "عدم موفقيت", "messages-processed": "پيام پردازش شد", - "errors-occurred": "خطاها رخ دادند" + "errors-occurred": "خطاها رخ دادند", + "all-events": "همه", + "entity-type": "نوع موجودي" }, "extension": { "extensions": "دنباله ها", 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 a5c25909b0..d3a6cf51a5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -1115,7 +1115,6 @@ "body": "Corps", "data": "Données", "data-type": "Type de données", - "entity": "Entité", "error": "erreur", "type-edge-event": "Downlink", "errors-occurred": "Des erreurs sont survenues", @@ -1138,7 +1137,9 @@ "type-debug-rule-node": "Debug", "type-error": "Erreur", "type-lc-event": "Evénement du cycle de vie", - "type-stats": "Statistiques" + "type-stats": "Statistiques", + "all-events": "Tout", + "entity-type": "Type d'entité" }, "extension": { "add": "Ajouter une extension", diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json index 363801ebc3..4050e48ad7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json +++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json @@ -915,7 +915,6 @@ "body": "Body", "method": "Metodo", "type": "Tipo", - "entity": "Entità", "message-id": "Id Messaggio", "message-type": "Tipo Messaggio", "data-type": "Tipo di dato", @@ -927,7 +926,9 @@ "success": "Success", "failed": "Failed", "messages-processed": "Messaggi elaborati", - "errors-occurred": "Si sono verificati degli errori" + "errors-occurred": "Si sono verificati degli errori", + "all-events": "Tutte", + "entity-type": "Tipo entità" }, "extension": { "extensions": "Estensioni", diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json index 88d666423c..f3696e9a23 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json @@ -768,7 +768,6 @@ "body": "体", "method": "方法", "type": "タイプ", - "entity": "エンティティ", "message-id": "メッセージID", "message-type": "メッセージタイプ", "data-type": "データ・タイプ", @@ -780,7 +779,9 @@ "success": "成功", "failed": "失敗", "messages-processed": "処理されたメッセージ", - "errors-occurred": "エラーが発生しました" + "errors-occurred": "エラーが発生しました", + "all-events": "すべて", + "entity-type": "エンティティタイプ" }, "extension": { "extensions": "拡張機能", diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json index 06bde1bb02..6f5ba73674 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -959,7 +959,6 @@ "body": "სხეული", "method": "მეთოდი", "type": "ტიპი", - "entity": "ობიექტი", "message-id": "მესიჯის-ID", "message-type": "მესიჯის ტიპი", "data-type": "მონაცემთა ტიპი", @@ -971,7 +970,9 @@ "success": "წარმატება", "failed": "ვერ მოხერხდა", "messages-processed": "შეტყობინებების დამუშავება", - "errors-occurred": "შეცდომები მოხდა" + "errors-occurred": "შეცდომები მოხდა", + "all-events": "ყველა", + "entity-type": "ობიექტის ტიპი" }, "extension": { "extensions": "დამატებითი აპი", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index f52c94a0f0..cbe962bc2a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -1329,7 +1329,6 @@ "body": "Body", "method": "방법", "type": "유형", - "entity": "개체", "message-id": "메시지 ID", "message-type": "메시지 유형", "data-type": "데이터 유형", @@ -1341,7 +1340,9 @@ "success": "성공", "failed": "실패", "messages-processed": "처리된 메시지", - "errors-occurred": "오류가 발생했습니다" + "errors-occurred": "오류가 발생했습니다", + "all-events": "모두", + "entity-type": "개체 유형" }, "extension": { "extensions": "확장", diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json index 76282308e7..c2b7ce6e79 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -894,7 +894,6 @@ "body": "Galvenā daļa", "method": "Metode", "type": "Tips", - "entity": "Vienība", "message-id": "Ziņojuma Id", "message-type": "Ziņojuma tips", "data-type": "Datu tips", @@ -906,7 +905,9 @@ "success": "Sekmīgi", "failed": "Kļūda", "messages-processed": "Ziņojumi apstrādāti", - "errors-occurred": "Kļūdas konstatētas" + "errors-occurred": "Kļūdas konstatētas", + "all-events": "Visi", + "entity-type": "Vienības tips" }, "extension": { "extensions": "Paplašinājumi", diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json index 325229b47b..7f5c7ce239 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json +++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json @@ -997,7 +997,6 @@ "body": "Corpo", "method": "Método", "type": "Tipo", - "entity": "Entidade", "message-id": "ID de mensagem", "message-type": "Tipo de Mensagem", "data-type": "Tipo de Dados", @@ -1009,7 +1008,9 @@ "success": "Êxito", "failed": "Falhou", "messages-processed": "Mensagens processadas", - "errors-occurred": "Erros" + "errors-occurred": "Erros", + "all-events": "Tudo", + "entity-type": "Tipo de entidade" }, "extension": { "extensions": "Extensões", diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json index 971ad02d00..80c9941f2a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json +++ b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json @@ -945,7 +945,6 @@ "body": "Corp", "method": "Metodă", "type": "Tip", - "entity": "Entitate", "message-id": "ID Mesaj", "message-type": "Tip Mesaj", "data-type": "Tip Date", @@ -957,7 +956,9 @@ "success": "Succes", "failed": "Eşuat", "messages-processed": "Mesaje procesate", - "errors-occurred": "Au apărut erori" + "errors-occurred": "Au apărut erori", + "all-events": "Toate", + "entity-type": "Tip Entitate" }, "extension": { "extensions": "Extensii", @@ -1174,7 +1175,7 @@ "entity-field": "Câmp Entitate", "access-token": "Token De Acces" }, - "stepper-text":{ + "stepper-text": { "select-file": "Selectează un fişier", "configuration": "Importă configurație", "column-type": "Selectează tipul de coloane", @@ -1796,4 +1797,4 @@ "language": { "language": "Limba" } -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index a78416a201..f015d01b18 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -948,7 +948,6 @@ "body": "Тело", "method": "Метод", "type": "Тип", - "entity": "Объект", "message-id": "ИД сообщения", "message-type": "Тип сообщения", "data-type": "Тип данных", @@ -960,7 +959,9 @@ "success": "Успех", "failed": "Неудача", "messages-processed": "Сообщения обработаны", - "errors-occurred": "Возникли ошибки" + "errors-occurred": "Возникли ошибки", + "all-events": "Все", + "entity-type": "Тип объекта" }, "extension": { "extensions": "Расширение", diff --git a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json index 4dffbc6be2..7dd60aaa2f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json +++ b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json @@ -1329,7 +1329,6 @@ "body": "Vsebina", "method": "Metoda", "type": "Vrsta", - "entity": "Entiteta", "message-id": "ID sporočila", "message-type": "Vrsta sporočila", "data-type": "Vrsta podatkov", @@ -1341,7 +1340,9 @@ "success": "Uspeh", "failed": "Ni uspelo", "messages-processed": "Obdelana sporočila", - "errors-occurred": "Prišlo je do napak" + "errors-occurred": "Prišlo je do napak", + "all-events": "Vse", + "entity-type": "Vrsta entitete" }, "extension": { "extensions": "Razširitve", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 06d259dace..dfe3dbbaef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -915,7 +915,6 @@ "body": "İçerik //(Body)", "method": "Yöntem", "type": "Tür", - "entity": "Varlık", "message-id": "Mesaj Kimliği", "message-type": "Mesaj tipi", "data-type": "Veri tipi", @@ -927,7 +926,9 @@ "success": "Başarı", "failed": "Başarısız oldu", "messages-processed": "Mesajlar işlendi", - "errors-occurred": "Hatalar oluştu" + "errors-occurred": "Hatalar oluştu", + "all-events": "Tümü", + "entity-type": "Öğe türü" }, "extension": { "extensions": "Uzantılar", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index f9f33d48b9..c45b19ad5f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -1201,7 +1201,6 @@ "out": "Out", "metadata": "Метадані", "message": "Повідомлення", - "entity": "Сутність", "message-id": "Id повідомлення", "message-type": "Тип повідомлення", "data-type": "Тип даних", @@ -1212,7 +1211,9 @@ "success": "Успіх", "failed": "Невдача", "messages-processed": "Повідомлення опрацьовані", - "errors-occurred": "Виникли помилки" + "errors-occurred": "Виникли помилки", + "all-events": "Всі", + "entity-type": "Тип сутності" }, "extension": { "extensions": "Розширення", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index bbd7f7b61d..b7a3078c3b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1334,7 +1334,6 @@ "body": "整体", "data": "数据", "data-type": "数据类型", - "entity": "实体", "error": "错误", "errors-occurred": "错误发生", "event": "事件", @@ -1356,7 +1355,9 @@ "type-debug-rule-node": "调试", "type-error": "错误", "type-lc-event": "生命周期事件", - "type-stats": "类型统计" + "type-stats": "类型统计", + "all-events": "全部", + "entity-type": "实体类型" }, "extension": { "add": "添加扩展", @@ -1943,7 +1944,7 @@ "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "set-root": "设置为根规则链", "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", - "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", + "set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?", "system": "系统" }, "rulenode": { diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 14f4268724..bc8374a1c1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -865,7 +865,6 @@ "body": "整體", "method": "方法", "type": "類型", - "entity": "實體", "message-id": "消息ID", "message-type": "消息類型", "data-type": "資料類型", @@ -877,7 +876,9 @@ "success": "成功", "failed": "失敗", "messages-processed": "消息處理", - "errors-occurred": "錯誤發生" + "errors-occurred": "錯誤發生", + "all-events": "所有", + "entity-type": "實體類型" }, "extension": { "extensions": "擴展",