diff --git a/application/src/main/data/json/demo/dashboards/gateways.json b/application/src/main/data/json/demo/dashboards/gateways.json deleted file mode 100644 index f4eb275c3c..0000000000 --- a/application/src/main/data/json/demo/dashboards/gateways.json +++ /dev/null @@ -1,1315 +0,0 @@ -{ - "title": "Gateways", - "image": null, - "mobileHide": false, - "mobileOrder": null, - "configuration": { - "widgets": { - "94715984-ae74-76e4-20b7-2f956b01ed80": { - "type": "latest", - "sizeX": 24, - "sizeY": 12, - "config": { - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085270425, - "endTimeMs": 1694171670425 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, - "showTitle": true, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "4px", - "settings": { - "enableSearch": true, - "displayPagination": true, - "defaultPageSize": 10, - "defaultSortOrder": "entityName", - "displayEntityName": true, - "displayEntityType": false, - "entitiesTitle": "List of gateways", - "enableSelectColumnDisplay": true, - "displayEntityLabel": false, - "entityNameColumnTitle": "Gateway Name" - }, - "title": "Devices gateway table", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400, - "padding": "5px 10px 5px 10px" - }, - "useDashboardTimewindow": false, - "showLegend": false, - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "active", - "type": "attribute", - "label": "Active", - "color": "#2196f3", - "settings": { - "columnWidth": "0px", - "useCellStyleFunction": true, - "useCellContentFunction": true, - "cellContentFunction": "value = '⬤';\nreturn value;", - "cellStyleFunction": "var color;\nif (value == 'false') {\n color = '#EB5757';\n} else {\n color = '#27AE60';\n}\nreturn {\n color: color,\n fontSize: '18px'\n};" - }, - "_hash": 0.3646047595211721 - }, - { - "name": "eventsSent", - "type": "timeseries", - "label": "Sent", - "color": "#4caf50", - "settings": { - "columnWidth": "0px", - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.7235710720767985 - }, - { - "name": "eventsProduced", - "type": "timeseries", - "label": "Events", - "color": "#f44336", - "settings": { - "columnWidth": "0px", - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.5085933386303254 - }, - { - "name": "LOGS", - "type": "timeseries", - "label": "Latest log", - "color": "#ffc107", - "settings": { - "columnWidth": "0px", - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.3504240371585048, - "postFuncBody": "if(value) {\n return value.substring(0, 31) + \"...\";\n} else {\n return '';\n}" - }, - { - "name": "RemoteLoggingLevel", - "type": "attribute", - "label": "Log level", - "color": "#607d8b", - "settings": { - "columnWidth": "0px", - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.9785994222542516 - } - ], - "entityAliasId": "3e0f533a-0db1-3292-184f-06e73535061a" - } - ], - "showTitleIcon": true, - "titleIcon": "list", - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "List device", - "widgetStyle": {}, - "displayTimewindow": true, - "actions": { - "headerButton": [ - { - "name": "Add device", - "icon": "add", - "type": "customPretty", - "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", - "customCss": "", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", - "customResources": [], - "id": "70837a9d-c3de-a9a7-03c5-dccd14998758" - } - ], - "actionCellButton": [ - { - "id": "78845501-234e-a452-6819-82b5b776e99f", - "name": "Configuration", - "icon": "settings", - "type": "openDashboardState", - "targetDashboardStateId": "__entityname__config", - "openRightLayout": false, - "setEntityId": true - }, - { - "id": "f6ffdba8-e40f-2b8d-851b-f5ecaf18606b", - "name": "Graphs", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "__entityname_grafic", - "setEntityId": true - }, - { - "name": "Edit device", - "icon": "edit", - "type": "customPretty", - "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", - "customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", - "customResources": [], - "id": "242671f3-76c6-6982-7acc-6f12addf0ccc" - }, - { - "name": "Delete device", - "icon": "delete", - "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenDeleteDeviceDialog();\n\nfunction openDeleteDeviceDialog() {\n let title = \"Are you sure you want to delete the device \" + entityName + \"?\";\n let content = \"Be careful, after the confirmation, the device and all related data will become unrecoverable!\";\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\n function (result) {\n if (result) {\n deleteDevice();\n }\n }\n );\n}\n\nfunction deleteDevice() {\n deviceService.deleteDevice(entityId.id).subscribe(\n function () {\n widgetContext.updateAliases();\n }\n );\n}\n", - "id": "862ec2b7-fbcf-376e-f85f-b77c07f36efa" - } - ], - "rowClick": [ - { - "id": "ad5fc7e1-5e60-e056-6940-a75a383466a1", - "name": "to_entityname__config", - "icon": "more_horiz", - "type": "openDashboardState", - "targetDashboardStateId": "__entityname__config", - "setEntityId": true, - "stateEntityParamName": "" - } - ] - } - }, - "id": "94715984-ae74-76e4-20b7-2f956b01ed80", - "typeFullFqn": "system.entity_admin_widgets.device_admin_table" - }, - "eadabbc7-519e-76fc-ba10-b3fe8c18da10": { - "type": "timeseries", - "sizeX": 14, - "sizeY": 13, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "LOGS", - "type": "timeseries", - "label": "LOGS", - "color": "#2196f3", - "settings": { - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.3496649158709739, - "postFuncBody": "return value.replace(/ - (.*) - \\[/gi, ' - $1 - [');" - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "realtime": { - "interval": 1000, - "timewindowMs": 2592000000 - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, - "showTitle": true, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "showTimestamp": true, - "displayPagination": true, - "defaultPageSize": 10 - }, - "title": "Debug events (logs)", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": false, - "showLegend": false, - "widgetStyle": {}, - "actions": {}, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10", - "typeFullFqn": "system.cards.timeseries_table" - }, - "f928afc4-30d1-8d0c-e3cf-777f9f9d1155": { - "type": "timeseries", - "sizeX": 17, - "sizeY": 4, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "opcuaEventsProduced", - "type": "timeseries", - "label": "opcuaEventsProduced", - "color": "#2196f3", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.1477920581839779 - }, - { - "name": "opcuaEventsSent", - "type": "timeseries", - "label": "opcuaEventsSent", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.6500957113784758 - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "realtime": { - "interval": 1000, - "timewindowMs": 120000 - }, - "aggregation": { - "type": "NONE", - "limit": 25000 - }, - "hideInterval": false, - "hideAggregation": false - }, - "showTitle": true, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "shadowSize": 4, - "fontColor": "#545454", - "fontSize": 10, - "xaxis": { - "showLabels": true, - "color": "#545454" - }, - "yaxis": { - "showLabels": true, - "color": "#545454" - }, - "grid": { - "color": "#545454", - "tickColor": "#DDDDDD", - "verticalLines": true, - "horizontalLines": true, - "outlineWidth": 1 - }, - "stack": false, - "tooltipIndividual": false, - "timeForComparison": "months", - "xaxisSecond": { - "axisPosition": "top", - "showLabels": true - }, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - } - }, - "title": "Real time information", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "mobileHeight": null, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "widgetStyle": {}, - "useDashboardTimewindow": false, - "displayTimewindow": true, - "actions": {} - }, - "id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155", - "typeFullFqn": "system.charts.basic_timeseries" - }, - "2a95b473-042d-59d0-2da2-40d0cccb6c8a": { - "type": "timeseries", - "sizeX": 7, - "sizeY": 7, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "eventsSent", - "type": "timeseries", - "label": "Events", - "color": "#2196f3", - "settings": { - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.8156044798125357 - }, - { - "name": "eventsProduced", - "type": "timeseries", - "label": "Produced", - "color": "#4caf50", - "settings": { - "useCellStyleFunction": false, - "useCellContentFunction": false - }, - "_hash": 0.6538259344015449 - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "realtime": { - "interval": 1000, - "timewindowMs": 604800000 - }, - "aggregation": { - "type": "NONE", - "limit": 200 - } - }, - "showTitle": true, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "showTimestamp": true, - "displayPagination": true, - "defaultPageSize": 6, - "hideEmptyLines": true - }, - "title": "Total Messages", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": false, - "showLegend": false, - "widgetStyle": {}, - "actions": {}, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true, - "legendConfig": { - "direction": "column", - "position": "bottom", - "showMin": false, - "showMax": false, - "showAvg": true, - "showTotal": false - } - }, - "id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a", - "typeFullFqn": "system.cards.timeseries_table" - }, - "aaa69366-aacc-9028-65aa-645c0f8533ec": { - "type": "timeseries", - "sizeX": 17, - "sizeY": 4, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "eventsSent", - "type": "timeseries", - "label": "eventsSent", - "color": "#2196f3", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.41414001784591314 - }, - { - "name": "eventsProduced", - "type": "timeseries", - "label": "eventsProduced", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.7819101846284422 - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, - "showTitle": true, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "shadowSize": 4, - "fontColor": "#545454", - "fontSize": 10, - "xaxis": { - "showLabels": true, - "color": "#545454" - }, - "yaxis": { - "showLabels": true, - "color": "#545454" - }, - "grid": { - "color": "#545454", - "tickColor": "#DDDDDD", - "verticalLines": true, - "horizontalLines": true, - "outlineWidth": 1 - }, - "stack": false, - "tooltipIndividual": false, - "timeForComparison": "months", - "xaxisSecond": { - "axisPosition": "top", - "showLabels": true - }, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - } - }, - "title": "History information", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "mobileHeight": null, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "widgetStyle": {}, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "actions": {} - }, - "id": "aaa69366-aacc-9028-65aa-645c0f8533ec", - "typeFullFqn": "system.charts.basic_timeseries" - }, - "ce5c7d01-a3ef-5cf0-4578-8505135c23a0": { - "type": "timeseries", - "sizeX": 17, - "sizeY": 4, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "bleEventsProduced", - "type": "timeseries", - "label": "bleEventsProduced", - "color": "#2196f3", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.5625165504526104 - }, - { - "name": "bleEventsSent", - "type": "timeseries", - "label": "bleEventsSent", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": true, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "tooltipValueFormatter": "", - "showSeparateAxis": false, - "axisTitle": "", - "axisPosition": "left", - "axisTicksFormatter": "", - "comparisonSettings": { - "showValuesForComparison": true, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.6817950080745288 - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "realtime": { - "interval": 5000, - "timewindowMs": 120000 - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": true, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "shadowSize": 4, - "fontColor": "#545454", - "fontSize": 10, - "xaxis": { - "showLabels": true, - "color": "#545454" - }, - "yaxis": { - "showLabels": true, - "color": "#545454" - }, - "grid": { - "color": "#545454", - "tickColor": "#DDDDDD", - "verticalLines": true, - "horizontalLines": true, - "outlineWidth": 1 - }, - "stack": false, - "tooltipIndividual": false, - "timeForComparison": "months", - "xaxisSecond": { - "axisPosition": "top", - "showLabels": true - }, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - } - }, - "title": "Real time information", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "mobileHeight": null, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "widgetStyle": {}, - "useDashboardTimewindow": false, - "displayTimewindow": true, - "actions": {} - }, - "id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0", - "typeFullFqn": "system.charts.basic_timeseries" - }, - "466f046d-6005-a168-b107-60fcb2469cd5": { - "type": "latest", - "sizeX": 7, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085270425, - "endTimeMs": 1694171670425 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": true, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "eventsTitle": "Gateway Events Form", - "eventsReg": [ - "EventsProduced", - "EventsSent" - ] - }, - "title": "Gateway events", - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": true, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "showLegend": false, - "actions": {} - }, - "id": "466f046d-6005-a168-b107-60fcb2469cd5", - "typeFullFqn": "system.gateway_widgets.attributes_card" - }, - "8fc32225-164f-3258-73f7-e6b6d959cf0b": { - "type": "latest", - "sizeX": 10, - "sizeY": 9, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085270425, - "endTimeMs": 1694171670425 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": true, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "gatewayTitle": "Gateway configuration (Single device)", - "readOnly": false - }, - "title": "New Gateway configuration (Single device)", - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": true, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "showLegend": false, - "actions": {} - }, - "id": "8fc32225-164f-3258-73f7-e6b6d959cf0b", - "typeFullFqn": "system.gateway_widgets.config_form_latest" - }, - "063fc179-c9fd-f952-e714-f24e9c43c05c": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2, - "config": { - "targetDeviceAliases": [], - "showTitle": false, - "backgroundColor": "#e6e7e8", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "requestTimeout": 5000, - "oneWayElseTwoWay": true, - "styleButton": { - "isRaised": true, - "isPrimary": false - }, - "methodParams": "{}", - "methodName": "gateway_reboot", - "buttonText": "GATEWAY REBOOT" - }, - "title": "New RPC Button", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "actions": {}, - "datasources": [], - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true, - "targetDeviceAliasIds": [ - "b2487e75-2fa4-f211-142c-434dfd50c70c" - ] - }, - "id": "063fc179-c9fd-f952-e714-f24e9c43c05c", - "typeFullFqn": "system.control_widgets.rpcbutton" - }, - "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": { - "type": "rpc", - "sizeX": 4, - "sizeY": 2, - "config": { - "targetDeviceAliases": [], - "showTitle": false, - "backgroundColor": "#e6e7e8", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "requestTimeout": 5000, - "oneWayElseTwoWay": true, - "styleButton": { - "isRaised": true, - "isPrimary": false - }, - "methodName": "gateway_restart", - "methodParams": "{}", - "buttonText": "GATEWAY RESTART" - }, - "title": "New RPC Button", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "actions": {}, - "datasources": [], - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true, - "targetDeviceAliasIds": [ - "b2487e75-2fa4-f211-142c-434dfd50c70c" - ] - }, - "id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7", - "typeFullFqn": "system.control_widgets.rpcbutton" - }, - "6770b6ba-eff8-df05-75f8-c1f9326d4842": { - "type": "latest", - "sizeX": 6, - "sizeY": 4, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "latitude", - "type": "attribute", - "label": "latitude", - "color": "#2196f3", - "settings": {}, - "_hash": 0.9743324774725604 - }, - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5530093635101525 - } - ], - "entityAliasId": "b2487e75-2fa4-f211-142c-434dfd50c70c" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085270425, - "endTimeMs": 1694171670425 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete", - "markerImageSize": 34, - "useColorFunction": false, - "markerImages": [], - "useMarkerImageFunction": false, - "color": "#fe7569", - "mapProvider": "OpenStreetMap.Mapnik", - "showTooltip": true, - "autocloseTooltip": true, - "defaultCenterPosition": [ - 0, - 0 - ], - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "defaultZoomLevel": 5 - }, - "title": "Gateway Location", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "tooltipAction": [ - { - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66", - "name": "delete", - "icon": "more_horiz", - "type": "custom", - "customFunction": "var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\n widgetContext.map.locations[0], {\n \"lat\": null,\n \"lng\": null\n }).then(function succes() {\n $rootScope.$broadcast('widgetForceReInit');\n });" - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "6770b6ba-eff8-df05-75f8-c1f9326d4842", - "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" - } - }, - "states": { - "main_gateway": { - "name": "Gateways", - "root": true, - "layouts": { - "main": { - "widgets": { - "94715984-ae74-76e4-20b7-2f956b01ed80": { - "sizeX": 24, - "sizeY": 12, - "row": 0, - "col": 0 - } - }, - "gridSettings": { - "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "backgroundSizeMode": "100%", - "autoFillHeight": true, - "mobileAutoFillHeight": false, - "mobileRowHeight": 70, - "margin": 10, - "outerMargin": true - } - } - } - }, - "__entityname__config": { - "name": "${entityName} Configuration", - "root": false, - "layouts": { - "main": { - "widgets": { - "eadabbc7-519e-76fc-ba10-b3fe8c18da10": { - "sizeX": 14, - "sizeY": 13, - "row": 0, - "col": 10 - }, - "8fc32225-164f-3258-73f7-e6b6d959cf0b": { - "sizeX": 10, - "sizeY": 9, - "row": 0, - "col": 0 - }, - "063fc179-c9fd-f952-e714-f24e9c43c05c": { - "sizeX": 4, - "sizeY": 2, - "row": 9, - "col": 0 - }, - "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": { - "sizeX": 4, - "sizeY": 2, - "row": 11, - "col": 0 - }, - "6770b6ba-eff8-df05-75f8-c1f9326d4842": { - "sizeX": 6, - "sizeY": 4, - "row": 9, - "col": 4 - } - }, - "gridSettings": { - "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "backgroundSizeMode": "100%", - "autoFillHeight": true, - "mobileAutoFillHeight": false, - "mobileRowHeight": 70, - "margin": 10, - "outerMargin": true - } - } - } - }, - "__entityname_grafic": { - "name": "${entityName} Details", - "root": false, - "layouts": { - "main": { - "widgets": { - "f928afc4-30d1-8d0c-e3cf-777f9f9d1155": { - "sizeX": 17, - "sizeY": 4, - "mobileHeight": null, - "row": 4, - "col": 7 - }, - "2a95b473-042d-59d0-2da2-40d0cccb6c8a": { - "sizeX": 7, - "sizeY": 7, - "row": 5, - "col": 0 - }, - "aaa69366-aacc-9028-65aa-645c0f8533ec": { - "sizeX": 17, - "sizeY": 4, - "mobileHeight": null, - "row": 0, - "col": 7 - }, - "ce5c7d01-a3ef-5cf0-4578-8505135c23a0": { - "sizeX": 17, - "sizeY": 4, - "mobileHeight": null, - "row": 8, - "col": 7 - }, - "466f046d-6005-a168-b107-60fcb2469cd5": { - "sizeX": 7, - "sizeY": 5, - "row": 0, - "col": 0 - } - }, - "gridSettings": { - "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "backgroundSizeMode": "auto 100%", - "autoFillHeight": true, - "mobileAutoFillHeight": true, - "mobileRowHeight": 70, - "margin": 10, - "outerMargin": true - } - } - } - } - }, - "entityAliases": { - "3e0f533a-0db1-3292-184f-06e73535061a": { - "id": "3e0f533a-0db1-3292-184f-06e73535061a", - "alias": "Gateways", - "filter": { - "type": "deviceType", - "resolveMultiple": true, - "deviceNameFilter": "", - "deviceTypes": [ - "gateway" - ] - } - }, - "b2487e75-2fa4-f211-142c-434dfd50c70c": { - "id": "b2487e75-2fa4-f211-142c-434dfd50c70c", - "alias": "Current Gateway", - "filter": { - "type": "stateEntity", - "resolveMultiple": false, - "stateEntityParamName": "", - "defaultStateEntity": null - } - } - }, - "timewindow": { - "realtime": { - "interval": 1000, - "timewindowMs": 86400000 - }, - "aggregation": { - "type": "NONE", - "limit": 25000 - }, - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false - }, - "settings": { - "stateControllerId": "entity", - "showTitle": true, - "showDashboardsSelect": true, - "showEntitiesSelect": true, - "showDashboardTimewindow": true, - "showDashboardExport": true, - "toolbarAlwaysOpen": true, - "titleColor": "rgba(0,0,0,0.870588)" - }, - "filters": {} - }, - "externalId": null, - "name": "Gateways" -} \ No newline at end of file diff --git a/application/src/main/data/json/edge/install_instructions/docker/instructions.md b/application/src/main/data/json/edge/install_instructions/docker/instructions.md index e308a38076..d8f9731889 100644 --- a/application/src/main/data/json/edge/install_instructions/docker/instructions.md +++ b/application/src/main/data/json/edge/install_instructions/docker/instructions.md @@ -51,7 +51,7 @@ services: - ~/.mytb-edge-logs:/var/log/tb-edge postgres: restart: always - image: "postgres:12" + image: "postgres:15" ports: - "5432" environment: diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 0829c612d0..14c09d3fc4 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -11,6 +11,7 @@ "widgetTypeFqns": [ "charts.basic_timeseries", "charts.state_chart", + "range_chart", "charts.timeseries_bars_flot", "cards.aggregated_value_card", "charts.bars", diff --git a/application/src/main/data/json/system/widget_types/doughnut.json b/application/src/main/data/json/system/widget_types/doughnut.json index f5a890ad3a..8764bbcace 100644 --- a/application/src/main/data/json/system/widget_types/doughnut.json +++ b/application/src/main/data/json/system/widget_types/doughnut.json @@ -2,7 +2,7 @@ "fqn": "doughnut", "name": "Doughnut", "deprecated": false, - "image": "data:image/png;base64,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", + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMkAAACgCAMAAACR47ilAAAA9lBMVEX////39/f5+fkAAAD////////////9/f36+vr///8Ihyv/X2r8/Pzy8vL39/f5+fnu7u4hISHs7Oz09PTq6urm5ubg8OXo6OgXjjjw9/Ki0q8nlkWrq6uDw5VktHr/6+3/c32Ty6LB4cqenp7/9fZ0dHQnlkbj4+N0vIf/19r/r7RFpWA2nlP/h4/R6dc9PT3b29v/aXNGpWBYWFgvLy//w8eQkJCy2r1VrG3IyMj/pavV1dXOzs7/m6LCwsL/ub7/fYb/4eP/zdG6urqCgoK2trb/kZhmZmY8PDykpKRKSkr/9fW728TO4tSDwpWi0rCFw5ZWrG24QEw9AAAACXRSTlOAgIAAcN+QOHDzcMjxAAALEklEQVR42tyae3PSQBTFq/V122ySbrIJYIBSUB4KgooKpWBRO75n/P5fxt2FugkLIZvNMuqZ0Zb/+M05596b6NHx8b378K/r/r3j46PjB/A/6MHx0UP4P/Tw6Aj+Dz06HIl1KzCiuwch4RTOWkZwDkji2J6HmDybyike5s5BSBgHcjH2Mcau6yLPK5rGNIkAQa7vehQH45CEPsXh5jiFsZgmESAYO7CWY7t4RVMgjFGS4G2lU2sEtCO2hzFAa3E1XXSvgYuykRVMMSymSIJK52XpZKWqxUywYDw6Xak/HXMcy/NJiDmLo81iYnaVK7XHJ3E1qCU2dE/jOr8Z1zlMSJ0pwpfCPSk3Xp5sqmQj34L3p5t6v2AwjstShmxbY5Jpk8huMAxZPxCG1uk29ZkzFgpDjJCGLwWnK6iWTrbru+tC/XSHrpgxHmPxbPW6GPDkImaHTOILT2T1uwCAQp93PyeKPonMIesx7QlA/3S3zscAFiaURQHFBElAOVJUsRFGfHal+HJNWXytiOn3pNw5SdVPy/FovGCcisL74rGI5UTRJ2mU0jBKtQu24xFDqV+dp6GMeixiIVYaYurpUg9W6VmnMijfXvQI+/zLXS97/dHOulBbEMEuTdhhSYQhMkVjsOUWtmGl6/HNDhpmi3rC9NNV/rUjUGVIirtCbSF8PAFTd3vU3tdZwtRQ9D0ZPN3C8awiMJIolMV1se+Ttr+iWd5sS9iSJ0wBRZ+kscWOzgB2yXIoDHv+5TQhtmHHDOipoeiTdGSOahl2y6JiMBzHZUlzLYBvY5ll2gInVEDRIynX1DgEDedZw/AZ0DvfVhYFFC2S8vNNkHcBZJQFaxjX9Ylvs4xtGccWe2xxjJMEm11/fAEq+uMMY7Eoy7mEouyKINEB6YhgKcFwFt6X3hYUqfbaJDKIliFCtywhYrZIKLz25kjkjryTDFFkcXFImC0Sik3SUPR3/AZIA3TEVyaLGAJYjJIoLUDE9WQUBU8U9kgpV7LkiGHals2E9QHYi0vHEEk1WZEA9CRu5ZBIKFMA3095jNQieVs4iLiVQ4qSfJe0ACutKjokwVMTIMBfuNKy2NBKolyDTVxkGyAxAiISRjyGkmy9G7pp+cpJUjUDwrUd5Yo9ee3Ll/oUHhgEEa4kaz9m+fIcBRLlbA2gSAkUB+rxvTKi+fL35ksxXVX1hageMJ9YyVdjN6tbch+JiidBHKQKxUugLOIoXbDD/flS8eR5vCRgRBZ7yRf6ADfJ+eXvN0XBk0r8RlFou/peCV1oxVvfy1J6BU+emi2JCBjCxIZusvT+3knMSZQtqYEBCVcQu1umCVMsZoo2iWyJmWyJ1ot8xSYxsnVJZEuqYFSs9cSGpVpTMpM8NTq35HwRC/pSU/RIZEsqYFIiX90NU9IXPZ/Cf5clK1OwbApOjVdGTy7MWiKz2LThCVOWgNKfHjOS1MxbIi8ViJvSB4ukmpItXWXzlsj5CpOm1HnndT2pmLdEnsQbTemBl/rwmI3kmXFLZFP4+FrG7ki+5zVJAmm9mxe/WaA1Eijd9HjxniiE6x0cSqtFP03GS5fkXbHhIplNSWzHvhQvdZKSIClDRk0+cbVBEmpmH8QWxOLFl2OKKft7cpEnXJiQaEKIc+tDU5nE2ozXmB3EWiSNnOGKhpxhNkSAZ82hA87lrC1IlOM1BTvMSSLXJFAmmUWzeYTIvDlDKJrPoktKorBSxPQSczg/SUl5LQoS1EQAnyY8XZh+HkaMJPP0Si7HFoTYc3I0Xt4mNWWSdgT86/OekMn8kwLJ6mJZxDcKxsjO78lb9ZoIkqEgoVRtdKlG4uN4URaA+IuJvCRVQTJQJsFfHID5nJNM5gAKJLwoIbQEyRU7vfJ4IhcelElgHg1nXxCg5iUafrmcfeEkSkU5F/91glXedpR7Ip+Pz0FJl5j9PZxPEP+B6J8ZmjnOTOF5izix15EjMbxyeVLSP7o07mEPponhpUMiwtUB05KHlwvj+NNWiPOTBHrvUDVJMIblxhhWJ5GvrrdgWjKJD9fJy6sQkgswL/leqcdI+ELJS1Ix8O9xSgsFBEmvKJIAlNT+80sUTWAt9CFiUznzYd/eIAkPTtJ+8/rJ+rdXZ1QvIs7x8TX78ARl9EQi8Q7sCWqyb78meXF29pV9ZLbQn69ffT47e5V9yUPssXF6cBLuwpok+t3e2fYkDgRx/DT3ZhpBil7dglSBQJtrrQnWqjHmHjBivMfv/2muHU4HqZDtTLca4j8xAaOmP3dmOjudHWw7BfCT/Oqn+BpuEUvHuookBzWTnA0bp0iCl53b0si2AYanp35ub7Y9LEFSKK/U7PFLJDM7oYSsDMmre/wTyRVetp+/fVRq277+/UQehYsbrQGDBBdlloWse3++HmmaeVHKuzN+WUOCWb1u102HRdJ4sDMl14A6xFjAzFa+Y52ozgySSNCukts0i7zThTUZcba/WBoWZJAuI6snEgxaPgbmpAEoXJdDXZJnJYlveS7M3zMqKq1wSB7sGWSa5ovyqMQeaRdXnu+0PrX4awIRr9yFJHTVVzlJejb7/70z3d1vE04Wdr+Q736BS+KReXFI0KzQyK7x5j6/M97qdhgsnnw+gd0DSUWiTyQxg2SYZY/DwwzkHnOW0/QwTezE1wzCeMp2sTDcqrtyRySQ2qgHP/eWe3ydTEGL5KXKHYNEWE0dpo+X69/OZqPp41ZldjYaNko8//3MrqbKK9xykZvsLlW414QuxlMHnuR736/ipw79V6kT7RTu8Dfo8BU9nZsAT9yGoibc6GZdRKLlKMqFWkQp/TGRHGGLKqyQ1rNfz0CLBPsptmhNOgLzErgJGpd2Z4EWiWuR6jIvbIGeGxclwvtSEghl3Zy8Zu589sQ5owNHNw4rMC1yk124W+qKEvd3uYpdsJeQUEaf6Sg3Lnn3YMD2eQFJuwl3zyNXU04ysLjPHiS3k72F9PEcC6ny3lQI+YvCj10t6vU4BnwwVwFJR7AoEpc/f8pU9sQ93MUer7ELJkVnanDazMl8BNNOPu+vEpKe6IQAt5G7nU+aOPqRvcM9ViUkrhKfCOKcoG3P16GhMUUKSfQWpV6nd39HSnm/2vvtVv6116zsdJPbFZ9u4sxF+Nto5UMwm80Kz85dsE+ccUFQP5uZMo4qT2aGFikCs6ItUbizm2FUe4I5JhDTPSw9RspaggQCqyZXGVgMEtr9lnR6FYMpOWNGnMQ1Yf2vrLEhr0cQRuWgFAkERlFonAujgFuSBCJjKATCGydQlsRR5lAIhFG+JRKGqxhAQRBugNxmTI4xhILOzr1pyaf5VFmhiBGEkUjwSDBrMbJb+aOsRXVdKCX51CtPamE0fpURtiQk4HSt6p3FiRggfBJCqdrC+ooBIicpooxljh9PLAaIkIRsQe4t5CFyEP5ky0trWT0ei1scRB4x/pJglm2vgDJmsBAHyXOBIeEEWIGNEQc7fMhJyO+LmnRATzRQnTsCUG5dhOK9PClZJ5ANAmW9oJAbOAxNr7Ym/RhWy+lfFn5PWh0Qz0Z3QmuF1CS4iN1lx3A6AVEsK4qBJSIRqdO11khFE8/r5fK8y2j9j/IzBbl1kbfIdcnwEMmamGIJB8AXrYlccfjKHHISYvFq5zBDgjbWZWCoQMBhhATVCcsuR98FmUyQoJwSMCFmnGIZIkEYDTNTHq2GXIZIkOYiCNUqiCjoOFC5kMSQ4kG/54VRV80But3QC/oXDhjS9sZ8+ufWO8mb0zvJ29PmkGx/+Aiboa0N+jTpzfmE74351PV/l9Yow3BWYVkAAAAASUVORK5CYII=", "description": "Displays the latest values of the attributes or time-series data in a doughnut chart. Supports numeric values only.", "descriptor": { "type": "latest", diff --git a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json index 1eeffcae6e..f76bf50e48 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_doughnut.json +++ b/application/src/main/data/json/system/widget_types/horizontal_doughnut.json @@ -2,7 +2,7 @@ "fqn": "horizontal_doughnut", "name": "Horizontal doughnut", "deprecated": false, - "image": "data:image/png;base64,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", + "image": "data:image/png;base64,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", "description": "Displays the latest values of the attributes or time-series data in a doughnut chart using horizontal layout. Supports numeric values only.", "descriptor": { "type": "latest", diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 814414a428..2e10796583 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/label_widget.json b/application/src/main/data/json/system/widget_types/label_widget.json index 9796fc4967..1c7c84109a 100644 --- a/application/src/main/data/json/system/widget_types/label_widget.json +++ b/application/src/main/data/json/system/widget_types/label_widget.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n \n self.ctx.imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).subscribe(\n function (transformedUrl) {\n self.ctx.$container.css('background', 'url(\"'+transformedUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', transformedUrl);\n }\n );\n\n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-label-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json index 922a776fe8..35f7d0dce3 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxnIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICA8cGF0aCBkPSJtMzguNCA2NDEuNzMgMzkzLjMxLTQuMjY4NCIgY29sb3I9IiMwMDAwMDAiIGZpbGw9IiMzMzYiLz4KICAgPHBhdGggZD0ibTM5LjAwOSA3MDQuNTQgNDg0LjE2LTYuNzA3NiIgY29sb3I9IiMwMDAwMDAiIGZpbGw9IiMzMzYiLz4KICAgPGcgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJtMzAzLjk2IDY4Mi41OSAxNDYuOCAxLjgyOTNjMTAuNTM0IDAuMTMxMjcgMTQuMzQ0LTIuNjM3NCAyNS40ODctNi4zNzI4IDEwLjQxMi0zLjQ5MDMgMzEuNDI0LTIuNjk5IDQxLjM4NS0yLjc3MzhsNDA1LjU2LTMuMDQ4OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im00MjYuMjIgMzE0Ljg5YzIuMDY3NSA5LjA1MjcgMS44NDE4IDUxLjcyOCA2LjUwNzkgNzQuODM1IDEuNjc0OCA4LjI5MzQgOC42NzUxIDE0LjA2NiAxMC4wNTUgMTQuODU5IDQuOTAxNSAyLjgxNDYgMTAuODE1IDguMTQ5OCAxMy4wNDYgMTYuMDg4IDYuNzU3OCAyNC4wNDYgMC44Nzk3MiA2OC40NTIgMC44Nzk3MiAxMTAuNjkgMCA2LjA5NzggMS42NjAxIDMwLjE0Ny0yLjE1NTkgMzMuOTYzLTIuNTQwOCAyLjU0MDgtMC4yODE2MyAxMi45OTEtMy40MzY4IDE2LjE0NGwtOS44NDk0IDkuODQzMWMtMTAuMzY3IDEwLjM2LTExLjU5IDYuNTI2MS0xNy43MzggMTguODIzLTMuNTY3NyA3LjEzNTQgNS40MDI0IDIwLjY3MiA3LjM1NDMgMjQuNTc2IDEuOTMyMSAzLjg2NDMtMS44NDIyIDQuNzc3Ny0xLjc5MjQgNy40NDYzIDAuMjUyODYgMTMuNTQ1IDIuMjk3NSAzNzMuOTMgMi4yOTc1IDM3My45MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im0zNjUuMjQgNTE5Ljc4IDQuMTE2IDUwMi4xNSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im0xMTYuNTMgNTA0LjE5IDMuODgwNiAzMTAuOTYiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzE3LjY4IDU3Ni40OSAxMzAuMTkgMS41MjQ0YzQuNTEwOCAzLjI0MTcgMjAuMzQ1IDcuOTY4NSAyNy43NDUgNC4yNjg0IDMuMTU1NS0xLjU3NzcgOS40MTktNS4zODgyIDE0LjAyNS0zLjk2MzYgNC4yNjcgMS4zMTk4IDYuMDE2OSAzLjExNjMgMTAuMzY2IDMuMDQ4OSAxMC4zMDQtMC4xNTk3NSAyMC4yMTIgMC4zODc0MSAzMC40ODkgMC4zMDQ4OSAxNzcuODktMS40MjgzIDM1Ni41OS0yLjEzMjUgNTM0Ljc3LTMuMDQ4OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im00NzUuMzEgNTgyLjg5Yy0zLjQ0NDIgMTEuMzUxLTIuMTAzNCAxMi40MzQgMy42NTg2IDIxLjAzNyAzLjc5NDQgNS42NjU2IDUwLjg2MyAxMy4wMzggNDEuNDY1IDI3LjEzNS0xMC41MzcgMTUuODA1LTIyLjg5Ny01LjQ3NzctMzMuODQzLTEuODI5My01LjQ1MjQgMS44MTc0LTcuMzQ5IDUuNDU2My0zLjY1ODcgOS4xNDY2IDIuODA2OCAyLjgwNjggNC4wNDggMS44MDQgNi41MjAzIDUuMTAwNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im00MzIuMDEgNjM2Ljg1YzguMzE5IDEzLjExIDE4Ljg0NiAxNC42MzUgMzUuNjcyIDE0LjYzNSAyLjkzODYgMCA3Ljg3LTAuOTMzNzEgMTAuNjcxIDAgMTEuMzU5IDMuNzg2NCAyNy4xOTQgMTAuMjc2IDM2LjIwMiAyMS4xMjkgOC4yOCA5Ljk3NjYgMTAuMjUzIDIzLjg4MyA3LjcwMiAzNy4xMDQtNi4xNjk5IDMxLjk4LTE2LjcxNCA1Ni45ODktMTkuMDQ0IDg2LjU2OS0xLjM0OCAxNy4xMTkgNC41MDk2IDIyLjUzNSAxMS4wNzEgMzMuOTI5IDEwLjY3IDE4LjUyNyA4LjcyNDUgMTQuMiA4LjU3MTQgMzQuMjg2LTAuMTM5NjMgMTguMzE5IDAgNjAuMjY0IDAgODAuNzE0IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUyOC41MSA2NTguOTZjLTEwLjY4MSAwLjkwNDU0LTcuMTA4LTUuNjAyNi0xMC44MjQtOC4wNzk2LTQuNzg0NS0zLjE4OTctMTIuMjI3LTEuMjUxLTE2Ljc2OS01Ljc5MjktMC42NjYxMi0wLjY2NjEyLTguODA5Ny00LjEwODgtMTAuMTc0LTIuNzQ0LTguMzY0NiA4LjM2NDYtMy4wNDg5IDIwLjU1Mi0zLjA0ODkgMzMuNTM4bDMuMDIyIDMzOS43IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxNy45OSA2NTEuMDNjLTAuMjIxNzEtMi43MDE4IDEuOTAzNS01LjU2MjEgMy4zNTM4LTcuMDEyNCAxLjc5OTQtMS43OTk0IDYuOTIyOSAxLjAwNDIgOC44NDE4LTAuOTE0NjYgMC4yODc2NS0wLjI4NzY2IDAuODQzMjktMTEuMTY0IDAuMjI4NjYtMTMuNTY4LTIuMDY0OC04LjA3NDItMi4wNTgtMjguNjU3LTIuMDU4LTM4LjcyMXYtNzMuMTczIiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUyOC42NiA2NzUuNDItMC40NTczMy0zMS41NTYiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNzY2LjMyIDU3OS42NCAwLjQzMTE4IDEzLjc5OGMzLjEzNjQgNC42NjkyIDMuMDE4MiA5LjYwMDcgMy4wMTgyIDE2LjM4NXYxNTcuMzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTEyMi45IDc2NS45MWMtMjAyLjMxIDQuNjkwNS00MDMuNzQtMS4xMTM4LTYwNS45NSAzLjM1MzktMTAuODY0IDAuMjQwMDItMy4zNjE1LTguNTg2My0yOC41MzctOC41ODYzIiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTg2MC4wMSA3MzcuMDdzLTk3LjQ0OCAwLjg1ODA2LTE0Ny41NyAwLjg1ODA2Yy01LjI2ODYgMC00LjUxNTUtOC4zMjk5LTcuMzAwOS04LjMyOTktMy45NzQ0IDAtOC42MjkyIDAuMDIwMS0xMC41MDkgMC4wMzU5LTIuMzM0OCAwLjAxOTctMS44MTA5IDguMzY2LTQuMTQ1OCA4LjM2NjktNDYuMTY5IDAuMDE4OC0xNjcuNDEtMS4zMDgtMTc1LjA1LTEuMzA4LTQuNDI5NiAwLTguNTc2My02LjQzOTctMTMuMTMyLTYuNDM5N2gtMTQuMzk1IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTY3NS4wMSA4MzEuMTctMC42MDk3OC01MjEuNzciIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNzk5LjQgMzEzLjA2IDEuMjE5NiA0OTUuODciIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNzM2LjU5IDMxMi40NS0xLjIxOTYgNzE2LjQ5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUzMC4wMyA2NDMuNDYgMzkyLjM3LTMuMDE4MiIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NTkuNDUgMzE0LjkgMS4yOTM1IDUwNy45OCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im05MjEuNTQgMzEwLjU5IDEuNzI0NyA1MzEuNzUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNzM2LjI5IDQ1My4zMSAxODUuNjgtMC4zMDQ4OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im0xMDYwLjggNTE0Ljk3cy0zNjMuMjgtNS42MjYyLTU0NC42NSAyLjUyMThjLTQuMTc3OCAwLjE4NzY5LTEyLjUgMS4wNjcxLTEyLjUgMS4wNjcxLTEuNTcxIDAuMTM0MS0yLjAwMDktMi4zMjUtMi41OTE2LTMuNTA2Mi0wLjA5NjctMC4xOTM0My03LjA2MDgtMS45MzM0LTcuNjIyMi0xLjM3Mi0yLjg5MzEgMi44OTMxLTcuNjMxNyA0LjI0ODctMTIuMTk2IDQuMTE2bC0xMTIuMDUtMy4yNTc4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTM5OS44MiA0NzkuNjEgMTEuNjQyIDUuNjA1M2MyLjk4NDEgMS40MzY4IDYuNTI4OC0wLjQ3NzEyIDkuOTE3MS0wLjQzMTE4bDEyNy4yIDEuNzI0NyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTkuMjUgNTE3LjEyLTAuNDMxMTktMjA4LjY5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTQzMi45MyAzODkuNzFjMTEuMDQ1IDAgMzUuNTMzIDAuNjE5MjcgNDIuNTgtMS4wMDQgOC40MDUyLTEuOTM2MiA3LjA2Ni02Ljk1MzggMTQuMTk3LTYuOTUzOCA3LjgwOTUgMCA2LjU0MjkgOC4wNjI0IDIwLjE0MiA4LjA2MjQgMTMuOTkxIDAgNDQuOTc3IDAuMzc4ODYgNjMuOTQgMC4zNzg4NiAxMi4wODQgMCA4Mi4wMDMgMC4zMDQ4OSA5My42MDEgMC4zMDQ4OSA4Ljc2MDUgMCAxMy4xNi0yLjI4ODMgMjEuMzQyLTcuMDEyNCA3LjE5NTItNC4xNTQxIDIuMDU0Ni05LjQ5MTQgMjAuNDI4LTguODQxOCAyMy4xNDUgMC44MTgzMyAxMi42NDMgMTQuMDI1IDMyLjMxOCAxNC4wMjVoMTUwLjkyYzE0LjMzMiAwLTQuMTE5MS0xMy4xMSAyOS4yNjktMTMuNDE1IiBjb2xvcj0iIzAwMDAwMCIvPgogICA8L2c+CiAgPC9nPgogIDxnIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODguNjc5NTciIHk9IjczNS44MDQ2MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjg2LjM5ODUiIHk9Ijc2NS42Mjg0MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjg2LjM5ODUiIHk9Ijc2NS42Mjg0MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkhhcnJ5PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzA5Ljg3MTgzIiB5PSItODAyLjM3NzM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Xb29kbGF3bjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU2Mi4xMTkyNiIgeT0iLTc3MS45NjgxNCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RWRnZW1vb3I8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTguMzA0ODciIHk9Ii03MzguMzY2NDYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk9saXZlcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Mi4xMjI4NiIgeT0iLTY3Ny4yMDM5OCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGlsbHNpZGU8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTcuMzI3MDkiIHk9Ii04NjIuNjE0MDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlJvY2s8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTg3LjM3MDE4IiB5PSItOTI2LjEzNjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5XZWJiPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg3MS4xNjEwMSIgeT0iNjM3LjU3NTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3MS4xNjEwMSIgeT0iNjM3LjU3NTIiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4NzMuODMyMjgiIHk9IjU3Ny4wMzI0NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3NS45NjY0OSIgeT0iNTEwLjI2MTgxIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODgxLjMxNjU5IiB5PSI0NTAuMTk4NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yOXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2MTUuNzkyNDgiIHk9IjM4Ny43NDcxNiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjM3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQ4NC42OTAzNyIgeT0iNDgxLjY1Mjg2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjV0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTYzLjA0Njc1IiB5PSI1MTMuMzYxMzMiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2NS45NzE1IiB5PSI1NzcuODk0ODQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU2NS45NzE1IiB5PSI1NzcuODk0ODQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4xM3RoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNDMzLjU4MDc1IiB5PSItNDYwLjczMzEyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5BbWlkb248L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MDUuNTMwOTgiIHk9Ii01MjMuNTQwMTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFya2Fuc2FzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzQ1LjQ4NDYyIiB5PSItMzcyLjU4NTk0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5XZXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTk2LjcyODMzIiB5PSItNTMxLjI1OTI4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5XYWNvPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTk1LjQzNDgxIiB5PSItMTIyLjUwMjk1IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXppZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg0NSkiIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2OTUuNzcyOTUiIHk9IjE2Mi4wNjg3NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlpvbzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMjQwLjU4OTk3IiB5PSI1NzQuNDQ1NDMiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4xM3RoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyMDYuMDMxNzUiIHk9IjUxMS42MzY2MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI2MjAuNDQzMTIiIHk9Ii01MDYuNjgyMTkiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjM3MC4yMTY4NiIgeT0iNjk4Ljg0MDA5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWFwbGU8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMzg0LjA4NDIiIHk9IjY4MC44NTEzOCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzg0LjA4NDIiIHk9IjY4MC44NTEzOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkRvdWdsYXM8L3RzcGFuPjwvdGV4dD4KICA8L2c+CiAgPHBhdGggZD0ibTM2Ny45MSAxMDEwaDI2My4wMiIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8ZyBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiPgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjczNi4yNjc0NiIgeT0iLTQzMy4xMzc3NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWVyaWRpYW48L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3Mi44MzIxNSIgeT0iNjQwLjIwNTI2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Q2VudHJhbDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzUuMDg5NjYiIHk9IjY3MC45MDM1IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NzUuMDg5NjYiIHk9IjY3MC45MDM1IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDk5LjQ4OTYyIiB5PSIxMDA4LjYwNjkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij40N3RoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyMTYuNjQ1NDMiIHk9IjcyNS45ODI5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPktlbGxvZ2c8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NzQuODc1NjEiIHk9Ii01MDguMTg5NzMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICA8L2c+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtMzY0LjE2IDY1OC40MyAyOTkuNTEtMS4wMTAyYzYuNDk4Ny0wLjAyMTkgNi45NzcyIDkuMjU0MSAxNi41OTYgOS4zOTI1IDEyLjA1NCAwLjE3MzM5IDI5LjExMS0wLjUzNTcyIDU0LjExNC0wLjMwMTEiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgeD0iMzczLjk5MzA0IiB5PSI5NDQuMzU3NTQiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzczLjk5MzA0IiB5PSI5NDQuMzU3NTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3ODAuODQ2MDciIHk9Ii00OTAuMjQ1OTciIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5TZW5lY2E8L3RzcGFuPjwvdGV4dD4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjcuNyA1MzcuMjEgMTQxLjI4LTEuMDEwMmM2LjQ5LTAuMDQ2NCAxMi43ODEgNy4yMzU0IDE5LjE5MyA3LjMyMzYgNTUuOTI0IDAuNzY4OSAxNTguNjktMC4xNzMzMyAyMzYuNTEtMS4wMTAyIDcuODM5Ni0wLjA4NDMgMjIuNjMxLTE5Ljg1NCAzMC4zMDUtMjAuNDU2IDIyLjI2Ni0xLjM1MTggNDUuMTc5LTAuNTA1MDcgNjcuNjgtMC41MDUwNyAxNi4xNDctMC42MzI0MSAzLjYxMDIgMjAuNzA4IDI2Ljc2OSAyMC43MDhsMjQzLjQ1LTEuMDEwMiIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSI2ODUuMjA4MTMiIHk9IjgyNy41MzA4MiIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODUuMjA4MTMiIHk9IjgyNy41MzA4MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTU1NC4yOSA3MjEuNDMtNC4yODU3LTE3OC4yMS0yLjg1NzEtNDQwLjcxLTAuMzU3MTQtNzkuMjg2IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjUyOS42MjUzMSIgeT0iLTU1MC44NDc3OCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiA8L2c+Cjwvc3ZnPgo=\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/range_chart.json b/application/src/main/data/json/system/widget_types/range_chart.json new file mode 100644 index 0000000000..981e3582c5 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/range_chart.json @@ -0,0 +1,31 @@ +{ + "fqn": "range_chart", + "name": "Range chart", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays changes to time-series data over time visualized with color ranges — for example, temperature or humidity readings.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-range-chart-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-range-chart-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"dataZoom\":true,\"rangeColors\":[{\"to\":-20,\"color\":\"#234CC7\"},{\"from\":-20,\"to\":0,\"color\":\"#305AD7\"},{\"from\":0,\"to\":10,\"color\":\"#7191EF\"},{\"from\":10,\"to\":20,\"color\":\"#FFA600\"},{\"from\":20,\"to\":30,\"color\":\"#F36900\"},{\"from\":30,\"to\":40,\"color\":\"#F04022\"},{\"from\":40,\"color\":\"#D81838\"}],\"outOfRangeColor\":\"#ccc\",\"fillArea\":true,\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"dd MMM yyyy HH:mm\",\"lastUpdateAgo\":false,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Range chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"°C\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + }, + "externalId": null, + "tags": [ + "range", + "color range", + "line chart" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/demo/dashboards/gateway.json b/application/src/main/data/json/tenant/dashboards/gateways.json similarity index 99% rename from application/src/main/data/json/demo/dashboards/gateway.json rename to application/src/main/data/json/tenant/dashboards/gateways.json index b35b480ef2..972aed299f 100644 --- a/application/src/main/data/json/demo/dashboards/gateway.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -1,5 +1,5 @@ { - "title": "Gateway", + "title": "ThingsBoard IoT Gateways", "image": null, "mobileHide": false, "mobileOrder": null, @@ -40,7 +40,7 @@ "color": "rgba(0, 0, 0, 0.87)", "padding": "4px", "settings": { - "entitiesTitle": "Gateway list", + "entitiesTitle": "Gateways list", "enableSearch": true, "enableSelectColumnDisplay": false, "enableStickyHeader": true, @@ -55,7 +55,7 @@ "defaultSortOrder": "entityName", "useRowStyleFunction": false }, - "title": "New Entities table", + "title": "Gateways list", "dropShadow": true, "enableFullscreen": false, "titleStyle": { @@ -571,11 +571,11 @@ "padding": "8px", "settings": { "useMarkdownTextFunction": true, - "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connecotrs\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Active | Inactive)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", + "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Active | Inactive)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", "applyDefaultMarkdownStyle": false, "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" }, - "title": "New Markdown/HTML Card", + "title": "Connectors", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", @@ -599,7 +599,7 @@ "actions": { "elementClick": [ { - "name": "Connecotrs", + "name": "Connectors", "icon": "more_horiz", "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -680,7 +680,7 @@ "defaultSortOrder": "-createdTime", "useRowStyleFunction": false }, - "title": "New Alarms table", + "title": "Alarms", "dropShadow": true, "enableFullscreen": false, "titleStyle": { @@ -1074,7 +1074,7 @@ } ] }, - "title": "New RPC remote shell", + "title": "RPC remote shell", "dropShadow": true, "enableFullscreen": true, "widgetStyle": { @@ -1485,7 +1485,7 @@ } ] }, - "title": "New RPC debug terminal", + "title": "RPC debug terminal", "dropShadow": true, "enableFullscreen": true, "widgetStyle": {}, @@ -1868,7 +1868,7 @@ "applyDefaultMarkdownStyle": false, "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: center;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}" }, - "title": "New Markdown/HTML Card", + "title": "Service command", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", @@ -1980,7 +1980,7 @@ "applyDefaultMarkdownStyle": false, "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}" }, - "title": "New Markdown/HTML Card", + "title": "General configuration", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", @@ -2145,7 +2145,7 @@ "applyDefaultMarkdownStyle": true, "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}" }, - "title": "New Markdown/HTML Card", + "title": "Gateway devices", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", @@ -4939,7 +4939,7 @@ "applyDefaultMarkdownStyle": false, "markdownCss": ".action-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}" }, - "title": "New Markdown/HTML Card", + "title": "Gateway commands", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", diff --git a/application/src/main/data/upgrade/3.6.1/schema_update.sql b/application/src/main/data/upgrade/3.6.1/schema_update.sql new file mode 100644 index 0000000000..ebcf20a2c5 --- /dev/null +++ b/application/src/main/data/upgrade/3.6.1/schema_update.sql @@ -0,0 +1,37 @@ +-- +-- Copyright © 2016-2023 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- RESOURCES UPDATE START + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'resource' AND column_name = 'data' AND data_type = 'bytea') THEN + ALTER TABLE resource RENAME COLUMN data TO base64_data; + ALTER TABLE resource ADD COLUMN data bytea; + UPDATE resource SET data = decode(base64_data, 'base64') WHERE base64_data IS NOT NULL; + ALTER TABLE resource DROP COLUMN base64_data; + END IF; + END; +$$; + +ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid; + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); + +-- RESOURCES UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 3d4340f6c9..607d5d8791 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.SmsService; @@ -322,6 +323,11 @@ public class ActorSystemContext { @Getter private NotificationExecutorService notificationExecutor; + @Lazy + @Autowired + @Getter + private PubSubRuleNodeExecutorProvider pubSubRuleNodeExecutorProvider; + @Autowired @Getter private SharedEventLoopGroupService sharedEventLoopGroupService; diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index 48425d58db..d69b9aec0b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -64,6 +64,9 @@ public class DeviceActor extends ContextAwareActor { case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: processor.processAttributesUpdate((DeviceAttributesEventNotificationMsg) msg); break; + case DEVICE_DELETE_TO_DEVICE_ACTOR_MSG: + ctx.stop(ctx.getSelf()); + break; case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: processor.processCredentialsUpdate(msg); break; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ca1d174322..28963b383a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -104,6 +104,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; @@ -538,6 +539,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getNotificationExecutor(); } + @Override + public PubSubRuleNodeExecutorProvider getPubSubRuleNodeExecutorProvider() { + return mainCtx.getPubSubRuleNodeExecutorProvider(); + } + @Override @Deprecated public ScriptEngine createJsScriptEngine(String script, String... argNames) { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index c9f5448df3..86f338e89d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -53,10 +53,13 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Slf4j public class TenantActor extends RuleChainManagerActor { @@ -65,8 +68,11 @@ public class TenantActor extends RuleChainManagerActor { private boolean isCore; private ApiUsageState apiUsageState; + private Set deletedDevices; + private TenantActor(ActorSystemContext systemContext, TenantId tenantId) { super(systemContext, tenantId); + this.deletedDevices = new HashSet<>(); } boolean cantFindTenant = false; @@ -221,6 +227,10 @@ public class TenantActor extends RuleChainManagerActor { if (!isCore) { log.warn("RECEIVED INVALID MESSAGE: {}", msg); } + if (deletedDevices.contains(msg.getDeviceId())) { + log.debug("RECEIVED MESSAGE FOR DELETED DEVICE: {}", msg); + return; + } TbActorRef deviceActor = getOrCreateDeviceActor(msg.getDeviceId()); if (priority) { deviceActor.tellWithHighPriority(msg); @@ -240,7 +250,8 @@ public class TenantActor extends RuleChainManagerActor { log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId); initRuleChains(); } - } else if (msg.getEntityId().getEntityType() == EntityType.EDGE) { + } + if (msg.getEntityId().getEntityType() == EntityType.EDGE) { EdgeId edgeId = new EdgeId(msg.getEntityId().getId()); EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService(); if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { @@ -249,7 +260,13 @@ public class TenantActor extends RuleChainManagerActor { Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId); edgeRpcService.updateEdge(tenantId, edge); } - } else if (isRuleEngine) { + } + if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent()) { + DeviceId deviceId = (DeviceId) msg.getEntityId(); + onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); + deletedDevices.add(deviceId); + } + if (isRuleEngine) { TbActorRef target = getEntityActorRef(msg.getEntityId()); if (target != null) { if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index 4d227345b1..e0d5d3e04e 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -38,6 +38,7 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; import org.thingsboard.server.utils.MiscUtils; @@ -51,6 +52,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +@TbCoreComponent @Service @Slf4j public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { diff --git a/application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java new file mode 100644 index 0000000000..7b3a752f57 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/TbRuleEngineSecurityConfiguration.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Order(SecurityProperties.BASIC_AUTH_ORDER) +@ConditionalOnExpression("'${service.type:null}'=='tb-rule-engine'") +public class TbRuleEngineSecurityConfiguration { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.headers().cacheControl().and().frameOptions().disable() + .and().cors().and().csrf().disable() + .authorizeRequests() + .antMatchers("/actuator/prometheus").permitAll() + .anyRequest().authenticated(); + return http.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 47a7eede4b..d9a2923d52 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; import org.thingsboard.server.service.security.permission.Operation; @@ -45,6 +46,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFIL import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -64,6 +67,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class AssetProfileController extends BaseController { private final TbAssetProfileService tbAssetProfileService; + private final ImageService imageService; @ApiOperation(value = "Get Asset Profile (getAssetProfileById)", notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " + @@ -74,10 +78,16 @@ public class AssetProfileController extends BaseController { @ResponseBody public AssetProfile getAssetProfileById( @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(ASSET_PROFILE_ID, strAssetProfileId); AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); - return checkAssetProfileId(assetProfileId, Operation.READ); + var result = checkAssetProfileId(assetProfileId, Operation.READ); + if (inlineImages) { + imageService.inlineImage(result); + } + return result; } @ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)", 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 365f1fc379..8226610a7d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -124,6 +124,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.ConstraintValidator; @@ -145,7 +146,6 @@ import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -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; @@ -272,7 +272,7 @@ public abstract class BaseController { protected PartitionService partitionService; @Autowired - protected TbResourceService resourceService; + protected ResourceService resourceService; @Autowired protected OtaPackageService otaPackageService; @@ -587,7 +587,7 @@ public abstract class BaseController { checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation); return; case TB_RESOURCE: - checkResourceId(new TbResourceId(entityId.getId()), operation); + checkResourceInfoId(new TbResourceId(entityId.getId()), operation); return; case OTA_PACKAGE: checkOtaPackageId(new OtaPackageId(entityId.getId()), operation); diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index e6b4235cf9..5c1842b4b4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -32,6 +32,9 @@ public class ControllerConstants { protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "See the 'Model' tab of the Response Class for more details. "; + + protected static final String INLINE_IMAGES = "inlineImages"; + protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; @@ -142,6 +145,8 @@ public class ControllerConstants { protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. "; protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. "; + protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority."; + protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId"; protected static final String RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES = "LWM2M_MODEL, JKS, PKCS_12, JS_MODULE"; diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index afc737ab88..502e40e929 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -74,6 +75,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -94,6 +97,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class DashboardController extends BaseController { private final TbDashboardService tbDashboardService; + private final ImageService imageService; public static final String DASHBOARD_ID = "dashboardId"; private static final String HOME_DASHBOARD_ID = "homeDashboardId"; @@ -153,10 +157,16 @@ public class DashboardController extends BaseController { @ResponseBody public Dashboard getDashboardById( @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + @PathVariable(DASHBOARD_ID) String strDashboardId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - return checkDashboardId(dashboardId, Operation.READ); + var result = checkDashboardId(dashboardId, Operation.READ); + if (inlineImages) { + imageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java index 83d4d16034..dca113621c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java @@ -106,7 +106,7 @@ public class DeviceConnectivityController extends BaseController { @RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}", method = RequestMethod.GET) @ResponseBody public JsonNode getGatewayLaunchCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) - @PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException { + @PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException { checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index c888d67252..3c07946309 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; @@ -52,6 +53,8 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -72,6 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class DeviceProfileController extends BaseController { private final TbDeviceProfileService tbDeviceProfileService; + private final ImageService imageService; @Autowired private TimeseriesService timeseriesService; @@ -85,10 +89,16 @@ public class DeviceProfileController extends BaseController { @ResponseBody public DeviceProfile getDeviceProfileById( @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); - return checkDeviceProfileId(deviceProfileId, Operation.READ); + var result = checkDeviceProfileId(deviceProfileId, Operation.READ); + if (inlineImages) { + imageService.inlineImage(result); + } + return result; } @ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)", diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java new file mode 100644 index 0000000000..7685640929 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -0,0 +1,283 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.Base64Utils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.thingsboard.server.common.data.ImageDescriptor; +import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbImageDeleteResult; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.resource.ImageCacheKey; +import org.thingsboard.server.service.resource.TbImageService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; + +@Slf4j +@RestController +@TbCoreComponent +@RequiredArgsConstructor +public class ImageController extends BaseController { + + private final ImageService imageService; + private final TbImageService tbImageService; + @Value("${cache.image.systemImagesBrowserTtlInMinutes:0}") + private int systemImagesBrowserTtlInMinutes; + @Value("${cache.image.tenantImagesBrowserTtlInMinutes:0}") + private int tenantImagesBrowserTtlInMinutes; + + private static final String IMAGE_URL = "/api/images/{type}/{key}"; + private static final String SYSTEM_IMAGE = "system"; + private static final String TENANT_IMAGE = "tenant"; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping("/api/image") + public TbResourceInfo uploadImage(@RequestPart MultipartFile file, + @RequestPart(required = false) String title) throws Exception { + SecurityUser user = getCurrentUser(); + TbResource image = new TbResource(); + image.setTenantId(user.getTenantId()); + accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image); + + image.setFileName(file.getOriginalFilename()); + if (StringUtils.isNotEmpty(title)) { + image.setTitle(title); + } else { + image.setTitle(file.getOriginalFilename()); + } + image.setResourceType(ResourceType.IMAGE); + ImageDescriptor descriptor = new ImageDescriptor(); + descriptor.setMediaType(file.getContentType()); + image.setDescriptorValue(descriptor); + image.setData(file.getBytes()); + return tbImageService.save(image, user); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(IMAGE_URL) + public TbResourceInfo updateImage(@PathVariable String type, + @PathVariable String key, + @RequestPart MultipartFile file) throws Exception { + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); + TbResource image = new TbResource(imageInfo); + image.setData(file.getBytes()); + image.setFileName(file.getOriginalFilename()); + image.updateDescriptor(ImageDescriptor.class, descriptor -> { + descriptor.setMediaType(file.getContentType()); + return descriptor; + }); + return tbImageService.save(image, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(IMAGE_URL + "/info") + public TbResourceInfo updateImageInfo(@PathVariable String type, + @PathVariable String key, + @RequestBody TbResourceInfo newImageInfo) throws ThingsboardException { + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); + imageInfo.setTitle(newImageInfo.getTitle()); + return tbImageService.save(imageInfo, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = IMAGE_URL, produces = "image/*") + public ResponseEntity downloadImage(@PathVariable String type, + @PathVariable String key, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { + return downloadIfChanged(type, key, etag, false); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = IMAGE_URL + "/export") + public ImageExportData exportImage(@PathVariable String type, @PathVariable String key) throws Exception { + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId()); + return new ImageExportData(descriptor.getMediaType(), imageInfo.getFileName(), imageInfo.getTitle(), imageInfo.getResourceKey(), Base64Utils.encodeToString(data)); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping("/api/image/import") + public TbResourceInfo importImage(@RequestBody ImageExportData imageData) throws Exception { + SecurityUser user = getCurrentUser(); + TbResource image = new TbResource(); + image.setTenantId(user.getTenantId()); + accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image); + + image.setFileName(imageData.getFileName()); + if (StringUtils.isNotEmpty(imageData.getTitle())) { + image.setTitle(imageData.getTitle()); + } else { + image.setTitle(imageData.getFileName()); + } + image.setResourceKey(imageData.getResourceKey()); + image.setResourceType(ResourceType.IMAGE); + ImageDescriptor descriptor = new ImageDescriptor(); + descriptor.setMediaType(imageData.getMediaType()); + image.setDescriptorValue(descriptor); + image.setData(Base64Utils.decodeFromString(imageData.getData())); + return tbImageService.save(image, user); + + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = IMAGE_URL + "/preview", produces = "image/png") + public ResponseEntity downloadImagePreview(@PathVariable String type, + @PathVariable String key, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { + return downloadIfChanged(type, key, etag, true); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(IMAGE_URL + "/info") + public TbResourceInfo getImageInfo(@PathVariable String type, + @PathVariable String key) throws ThingsboardException { + return checkImageInfo(type, key, Operation.READ); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping("/api/images") + public PageData getImages(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION) + @RequestParam(required = false) boolean includeSystemImages, + @ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + // PE: generic permission + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getTenantId(); + if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) { + return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink)); + } else { + return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink)); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @DeleteMapping(IMAGE_URL) + public ResponseEntity deleteImage(@PathVariable String type, + @PathVariable String key, + @RequestParam(name = "force", required = false) boolean force) throws ThingsboardException { + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.DELETE); + TbImageDeleteResult result = tbImageService.delete(imageInfo, getCurrentUser(), force); + return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result); + } + + private ResponseEntity downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException { + ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview); + if (StringUtils.isNotEmpty(etag)) { + etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification + if (etag.equals(tbImageService.getETag(cacheKey))) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); + } + } + TenantId tenantId = getTenantId(); + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); + String fileName = imageInfo.getFileName(); + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + byte[] data; + if (preview) { + descriptor = descriptor.getPreviewDescriptor(); + data = imageService.getImagePreview(tenantId, imageInfo.getId()); + } else { + data = imageService.getImageData(tenantId, imageInfo.getId()); + } + tbImageService.putETag(cacheKey, descriptor.getEtag()); + var result = ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) + .header("x-filename", fileName) + .header("Content-Type", descriptor.getMediaType()) + .contentLength(data.length) + .eTag(descriptor.getEtag()); + if (systemImagesBrowserTtlInMinutes > 0 && imageInfo.getTenantId().isSysTenantId()) { + result.cacheControl(CacheControl.maxAge(systemImagesBrowserTtlInMinutes, TimeUnit.MINUTES)); + } else if (tenantImagesBrowserTtlInMinutes > 0 && !imageInfo.getTenantId().isSysTenantId()) { + result.cacheControl(CacheControl.maxAge(tenantImagesBrowserTtlInMinutes, TimeUnit.MINUTES)); + } else { + result.cacheControl(CacheControl.noCache()); + } + return result.body(new ByteArrayResource(data)); + } + + private TbResourceInfo checkImageInfo(String imageType, String key, Operation operation) throws ThingsboardException { + TenantId tenantId = getTenantId(imageType); + TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key); + checkEntity(getCurrentUser(), checkNotNull(imageInfo), operation); + return imageInfo; + } + + private TenantId getTenantId(String imageType) throws ThingsboardException { + TenantId tenantId; + if (imageType.equals(TENANT_IMAGE)) { + tenantId = getTenantId(); + } else if (imageType.equals(SYSTEM_IMAGE)) { + tenantId = TenantId.SYS_TENANT_ID; + } else { + throw new IllegalArgumentException("Invalid image URL"); + } + return tenantId; + } + +} \ No newline at end of file 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 94e318f37c..5a1758f860 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -19,6 +19,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -26,16 +27,17 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; 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.ResourceType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -50,8 +52,11 @@ import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.Base64; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; @@ -87,15 +92,14 @@ public class TbResourceController extends BaseController { @ApiOperation(value = "Download Resource (downloadResource)", notes = "Download Resource based on the provided Resource Id." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET) - @ResponseBody - public ResponseEntity downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + @GetMapping(value = "/resource/{resourceId}/download") + public ResponseEntity downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); TbResource tbResource = checkResourceId(resourceId, Operation.READ); - ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); + ByteArrayResource resource = new ByteArrayResource(tbResource.getData()); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) .header("x-filename", tbResource.getFileName()) @@ -106,42 +110,38 @@ public class TbResourceController extends BaseController { @ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET, produces = "application/xml") - @ResponseBody - public ResponseEntity downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable(RESOURCE_ID) String strResourceId, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + @GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml") + public ResponseEntity downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag); } @ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-pkcs12") - @ResponseBody - public ResponseEntity downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable(RESOURCE_ID) String strResourceId, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + public ResponseEntity downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag); } @ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-java-keystore") - @ResponseBody - public ResponseEntity downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable(RESOURCE_ID) String strResourceId, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + @GetMapping(value = "/resource/jks/{resourceId}/download", produces = "application/x-java-keystore") + public ResponseEntity downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag); } @ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET, produces = "application/javascript") - @ResponseBody - public ResponseEntity downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable(RESOURCE_ID) String strResourceId, - @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { + @GetMapping(value = "/resource/js/{resourceId}/download", produces = "application/javascript") + public ResponseEntity downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag); } @@ -150,8 +150,7 @@ public class TbResourceController extends BaseController { RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, produces = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/info/{resourceId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/resource/info/{resourceId}") public TbResourceInfo getResourceInfoById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); @@ -162,10 +161,10 @@ public class TbResourceController extends BaseController { @ApiOperation(value = "Get Resource (getResourceById)", notes = "Fetch the Resource object based on the provided Resource Id. " + RESOURCE_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, - produces = "application/json") + produces = "application/json", hidden = true) + @Deprecated // resource's data should be fetched with a download request @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/resource/{resourceId}") public TbResource getResourceById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); @@ -184,13 +183,12 @@ public class TbResourceController extends BaseController { produces = "application/json", consumes = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource", method = RequestMethod.POST) - @ResponseBody - public TbResource saveResource(@ApiParam(value = "A JSON value representing the Resource.") - @RequestBody TbResource resource) throws Exception { + @PostMapping(value = "/resource") + public TbResourceInfo saveResource(@ApiParam(value = "A JSON value representing the Resource.") + @RequestBody TbResource resource) throws Exception { resource.setTenantId(getTenantId()); checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); - return tbResourceService.save(resource, getCurrentUser()); + return new TbResourceInfo(tbResourceService.save(resource, getCurrentUser())); } @ApiOperation(value = "Get Resource Infos (getResources)", @@ -198,8 +196,7 @@ public class TbResourceController extends BaseController { PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, produces = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/resource") public PageData getResources(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -215,9 +212,14 @@ public class TbResourceController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder(); filter.tenantId(getTenantId()); + Set resourceTypes = new HashSet<>(); if (StringUtils.isNotEmpty(resourceType)) { - filter.resourceType(ResourceType.valueOf(resourceType)); + resourceTypes.add(ResourceType.valueOf(resourceType)); + } else { + Collections.addAll(resourceTypes, ResourceType.values()); + resourceTypes.remove(ResourceType.IMAGE); } + filter.resourceTypes(resourceTypes); if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink)); } else { @@ -225,13 +227,36 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get All Resource Infos (getAllResources)", + notes = "Returns a page of Resource Info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/resource/tenant") + public PageData getTenantResources(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(getTenantId()) + .resourceTypes(EnumSet.allOf(ResourceType.class)) + .build(); + return checkNotNull(resourceService.findTenantResourcesByTenantId(filter, pageLink)); + } + @ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjectsPage)", notes = "Returns a page of LwM2M objects parsed from Resources with type 'LWM2M_MODEL' owned by tenant or sysadmin. " + PAGE_DATA_PARAMETERS + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/resource/lwm2m/page", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/resource/lwm2m/page") public List getLwm2mListObjectsPage(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -243,7 +268,7 @@ public class TbResourceController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = new PageLink(pageSize, page, textSearch); - return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); + return checkNotNull(tbResourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); } @ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjects)", @@ -251,22 +276,20 @@ public class TbResourceController extends BaseController { "You can specify parameters to filter the results. " + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/resource/lwm2m", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/resource/lwm2m") public List getLwm2mListObjects(@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES, required = true) @RequestParam String sortOrder, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES, required = true) @RequestParam String sortProperty, @ApiParam(value = "LwM2M Object ids.", required = true) @RequestParam(required = false) String[] objectIds) throws ThingsboardException { - return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); + return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); } @ApiOperation(value = "Delete Resource (deleteResource)", notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/resource/{resourceId}") public void deleteResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) @PathVariable("resourceId") String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); @@ -275,12 +298,12 @@ public class TbResourceController extends BaseController { tbResourceService.delete(tbResource, getCurrentUser()); } - private ResponseEntity downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException { + private ResponseEntity downloadResourceIfChanged(ResourceType resourceType, String strResourceId, String etag) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); - if (etag != null) { TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ); + etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification if (etag.equals(tbResourceInfo.getEtag())) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED) .eTag(tbResourceInfo.getEtag()) @@ -289,15 +312,16 @@ public class TbResourceController extends BaseController { } TbResource tbResource = checkResourceId(resourceId, Operation.READ); - ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); + ByteArrayResource resource = new ByteArrayResource(tbResource.getData()); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) .header("x-filename", tbResource.getFileName()) .contentLength(resource.contentLength()) - .header("Content-Type", type.getMediaType()) + .header("Content-Type", resourceType.getMediaType()) .cacheControl(CacheControl.noCache()) .eTag(tbResource.getEtag()) .body(resource); } + } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index 5f88752ff4..55f4f3afbd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -74,7 +74,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.exception.InvalidParametersException; import org.thingsboard.server.exception.UncheckedApiException; diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 6fbee00f7f..1fe492d692 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -133,6 +133,7 @@ public class TenantProfileController extends BaseController { " \"maxRuleChains\": 0,\n" + " \"maxResourcesInBytes\": 0,\n" + " \"maxOtaPackagesInBytes\": 0,\n" + + " \"maxResourceSize\": 0,\n" + " \"transportTenantMsgRateLimit\": \"1000:1,20000:60\",\n" + " \"transportTenantTelemetryMsgRateLimit\": \"1000:1,20000:60\",\n" + " \"transportTenantTelemetryDataPointsRateLimit\": \"1000:1,20000:60\",\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index f90a196edf..fa18fe4265 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.security.permission.Operation; @@ -53,6 +54,8 @@ import java.util.Collections; import java.util.List; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -72,6 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ public class WidgetTypeController extends AutoCommitController { private final TbWidgetTypeService tbWidgetTypeService; + private final ImageService imageService; private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory."; private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " + @@ -92,10 +96,16 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public WidgetTypeDetails getWidgetTypeById( @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + @PathVariable("widgetTypeId") String strWidgetTypeId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); - return checkWidgetTypeId(widgetTypeId, Operation.READ); + var result = checkWidgetTypeId(widgetTypeId, Operation.READ); + if (inlineImages) { + imageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", @@ -216,7 +226,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET) @ResponseBody public List getBundleWidgetTypes( @@ -248,14 +258,21 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypes)", notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET) @ResponseBody public List getBundleWidgetTypesDetails( @ApiParam(value = "Widget Bundle Id", required = true) - @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + @RequestParam("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages + ) throws ThingsboardException { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + var result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + if (inlineImages) { + result.forEach(imageService::inlineImages); + } + return result; } @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", @@ -369,6 +386,7 @@ public class WidgetTypeController extends AutoCommitController { String typeFqn = fqn.substring(scopeQualifier.length() + 1); WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, typeFqn); checkNotNull(widgetType); + accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType); return widgetType; } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index f62d1b7b05..1fe203249f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService; import org.thingsboard.server.service.security.permission.Operation; @@ -47,6 +48,8 @@ import java.util.List; import java.util.Set; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; +import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -66,6 +69,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDL public class WidgetsBundleController extends BaseController { private final TbWidgetsBundleService tbWidgetsBundleService; + private final ImageService imageService; private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. "; private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types"; @@ -78,10 +82,16 @@ public class WidgetsBundleController extends BaseController { @ResponseBody public WidgetsBundle getWidgetsBundleById( @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + @PathVariable("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter("widgetsBundleId", strWidgetsBundleId); WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - return checkWidgetsBundleId(widgetsBundleId, Operation.READ); + var result = checkWidgetsBundleId(widgetsBundleId, Operation.READ); + if (inlineImages) { + imageService.inlineImage(result); + } + return result; } @ApiOperation(value = "Create Or Update Widget Bundle (saveWidgetsBundle)", @@ -196,9 +206,9 @@ public class WidgetsBundleController extends BaseController { } else { TenantId tenantId = getCurrentUser().getTenantId(); if (tenantOnly != null && tenantOnly) { - return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); } else { - return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); } } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 0730bca8f8..3fc68b9cc3 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -112,6 +112,8 @@ public class ThingsboardInstallService { } else if ("3.0.1-cassandra".equals(upgradeFromVersion)) { log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ..."); latestMigrateService.migrate(); + } else if (upgradeFromVersion.equals("3.6.2-images")) { + installScripts.updateImages(); } else { switch (upgradeFromVersion) { case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion @@ -266,6 +268,15 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.6.0 to 3.6.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.0"); dataUpdateService.updateData("3.6.0"); + case "3.6.1": + log.info("Upgrading ThingsBoard from version 3.6.1 to 3.6.2 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.6.1"); + installScripts.loadSystemImages(); + if (!getEnv("SKIP_IMAGES_MIGRATION", false)) { + installScripts.updateImages(); + } else { + log.info("Skipping images migration. Run the upgrade with fromVersion as '3.6.2-images' to migrate"); + } //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: @@ -319,6 +330,7 @@ public class ThingsboardInstallService { // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); installScripts.loadSystemLwm2mResources(); + installScripts.loadSystemImages(); if (loadDemo) { log.info("Loading demo data..."); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 0d2b6675c9..c62bd81242 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -42,7 +42,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; -import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetProfileEdgeProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 7318b1eb24..99f126c4a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -623,37 +623,37 @@ public final class EdgeGrpcSession implements Closeable { case ASSET: return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion); case ENTITY_VIEW: - return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent); + return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent, this.edgeVersion); case DASHBOARD: - return ctx.getDashboardProcessor().convertDashboardEventToDownlink(edgeEvent); + return ctx.getDashboardProcessor().convertDashboardEventToDownlink(edgeEvent, this.edgeVersion); case CUSTOMER: - return ctx.getCustomerProcessor().convertCustomerEventToDownlink(edgeEvent); + return ctx.getCustomerProcessor().convertCustomerEventToDownlink(edgeEvent, this.edgeVersion); case RULE_CHAIN: - return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent); + return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent, this.edgeVersion); case RULE_CHAIN_METADATA: return ctx.getRuleChainProcessor().convertRuleChainMetadataEventToDownlink(edgeEvent, this.edgeVersion); case ALARM: - return ctx.getAlarmProcessor().convertAlarmEventToDownlink(edgeEvent); + return ctx.getAlarmProcessor().convertAlarmEventToDownlink(edgeEvent, this.edgeVersion); case USER: - return ctx.getUserProcessor().convertUserEventToDownlink(edgeEvent); + return ctx.getUserProcessor().convertUserEventToDownlink(edgeEvent, this.edgeVersion); case RELATION: - return ctx.getRelationProcessor().convertRelationEventToDownlink(edgeEvent); + return ctx.getRelationProcessor().convertRelationEventToDownlink(edgeEvent, this.edgeVersion); case WIDGETS_BUNDLE: - return ctx.getWidgetBundleProcessor().convertWidgetsBundleEventToDownlink(edgeEvent); + return ctx.getWidgetBundleProcessor().convertWidgetsBundleEventToDownlink(edgeEvent, this.edgeVersion); case WIDGET_TYPE: return ctx.getWidgetTypeProcessor().convertWidgetTypeEventToDownlink(edgeEvent, this.edgeVersion); case ADMIN_SETTINGS: - return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent); + return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent, this.edgeVersion); case OTA_PACKAGE: - return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent); + return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion); case TB_RESOURCE: - return ctx.getResourceEdgeProcessor().convertResourceEventToDownlink(edgeEvent); + return ctx.getResourceEdgeProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion); case QUEUE: - return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent); + return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion); case TENANT: - return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.getEdgeVersion()); + return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); case TENANT_PROFILE: - return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.getEdgeVersion()); + return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); default: log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); return null; @@ -670,52 +670,52 @@ public final class EdgeGrpcSession implements Closeable { } if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { - result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); + result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); + result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { - result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg)); + result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsgFromEdge(edge.getTenantId(), edge.getId(), deviceCredentialsUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { - result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); + result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getAssetUpdateMsgCount() > 0) { for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { - result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); + result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { - result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg)); + result.add(ctx.getAlarmProcessor().processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { - result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); + result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getRelationUpdateMsgCount() > 0) { for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { - result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg)); + result.add(ctx.getRelationProcessor().processRelationMsgFromEdge(edge.getTenantId(), edge, relationUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { - result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); + result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getResourceUpdateMsgCount() > 0) { for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { - result.add(ctx.getResourceEdgeProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); + result.add(ctx.getResourceEdgeProcessor().processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg, this.edgeVersion)); } } if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java deleted file mode 100644 index 86ffc6dd5c..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.constructor; - -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Component -@TbCoreComponent -public class AssetMsgConstructor { - - public AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) { - AssetUpdateMsg.Builder builder = AssetUpdateMsg.newBuilder() - .setMsgType(msgType) - .setIdMSB(asset.getUuidId().getMostSignificantBits()) - .setIdLSB(asset.getUuidId().getLeastSignificantBits()) - .setName(asset.getName()) - .setType(asset.getType()); - if (asset.getLabel() != null) { - builder.setLabel(asset.getLabel()); - } - if (asset.getCustomerId() != null) { - builder.setCustomerIdMSB(asset.getCustomerId().getId().getMostSignificantBits()); - builder.setCustomerIdLSB(asset.getCustomerId().getId().getLeastSignificantBits()); - } - if (asset.getAssetProfileId() != null) { - builder.setAssetProfileIdMSB(asset.getAssetProfileId().getId().getMostSignificantBits()); - builder.setAssetProfileIdLSB(asset.getAssetProfileId().getId().getLeastSignificantBits()); - } - if (asset.getAdditionalInfo() != null) { - builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); - } - return builder.build(); - } - - public AssetUpdateMsg constructAssetDeleteMsg(AssetId assetId) { - return AssetUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(assetId.getId().getMostSignificantBits()) - .setIdLSB(assetId.getId().getLeastSignificantBits()).build(); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java deleted file mode 100644 index d8e19ee854..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.constructor; - -import com.google.protobuf.ByteString; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import java.nio.charset.StandardCharsets; - -@Component -@TbCoreComponent -public class DeviceProfileMsgConstructor { - - @Autowired - private DataDecodingEncodingService dataDecodingEncodingService; - - public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { - DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder() - .setMsgType(msgType) - .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) - .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) - .setName(deviceProfile.getName()) - .setDefault(deviceProfile.isDefault()) - .setType(deviceProfile.getType().name()) - .setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile.getProfileData()))); - if (deviceProfile.getDefaultQueueName() != null) { - builder.setDefaultQueueName(deviceProfile.getDefaultQueueName()); - } - 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()); - } - if (deviceProfile.getImage() != null) { - builder.setImage(ByteString.copyFrom(deviceProfile.getImage().getBytes(StandardCharsets.UTF_8))); - } - if (deviceProfile.getFirmwareId() != null) { - builder.setFirmwareIdMSB(deviceProfile.getFirmwareId().getId().getMostSignificantBits()) - .setFirmwareIdLSB(deviceProfile.getFirmwareId().getId().getLeastSignificantBits()); - } - if (deviceProfile.getSoftwareId() != null) { - builder.setSoftwareIdMSB(deviceProfile.getSoftwareId().getId().getMostSignificantBits()) - .setSoftwareIdLSB(deviceProfile.getSoftwareId().getId().getLeastSignificantBits()); - } - if (deviceProfile.getDefaultEdgeRuleChainId() != null) { - builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) - .setDefaultRuleChainIdLSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); - } - if (deviceProfile.getDefaultDashboardId() != null) { - builder.setDefaultDashboardIdMSB(deviceProfile.getDefaultDashboardId().getId().getMostSignificantBits()) - .setDefaultDashboardIdLSB(deviceProfile.getDefaultDashboardId().getId().getLeastSignificantBits()); - } - return builder.build(); - } - - public DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { - return DeviceProfileUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(deviceProfileId.getId().getMostSignificantBits()) - .setIdLSB(deviceProfileId.getId().getLeastSignificantBits()).build(); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructor.java new file mode 100644 index 0000000000..071f2bc25d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructor.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +public interface MsgConstructor { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructorFactory.java new file mode 100644 index 0000000000..93f08c27ca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/MsgConstructorFactory.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public abstract class MsgConstructorFactory { + + @Autowired + protected T v1Constructor; + + @Autowired + protected U v2Constructor; + + public MsgConstructor getMsgConstructorByEdgeVersion(EdgeVersion edgeVersion) { + switch (edgeVersion) { + case V_3_3_0: + case V_3_3_3: + case V_3_4_0: + case V_3_6_0: + case V_3_6_1: + return v1Constructor; + case V_3_6_2: + default: + return v2Constructor; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java deleted file mode 100644 index 9fdb1bf069..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.constructor; - -import com.google.protobuf.ByteString; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; - -@Component -@TbCoreComponent -public class TenantProfileMsgConstructor { - - @Autowired - private DataDecodingEncodingService dataDecodingEncodingService; - - public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) { - ByteString profileData = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_1) ? - ByteString.empty() : ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData())); - TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder() - .setMsgType(msgType) - .setIdMSB(tenantProfile.getId().getId().getMostSignificantBits()) - .setIdLSB(tenantProfile.getId().getId().getLeastSignificantBits()) - .setName(tenantProfile.getName()) - .setDefault(tenantProfile.isDefault()) - .setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine()) - .setProfileDataBytes(profileData); - if (tenantProfile.getDescription() != null) { - builder.setDescription(tenantProfile.getDescription()); - } - return builder.build(); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java deleted file mode 100644 index 5b615ca9f8..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.constructor; - -import com.google.protobuf.ByteString; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; -import org.thingsboard.server.queue.util.TbCoreComponent; - -import java.nio.charset.StandardCharsets; -import java.util.List; - -@Component -@TbCoreComponent -public class WidgetsBundleMsgConstructor { - - public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets) { - WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() - .setMsgType(msgType) - .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) - .setIdLSB(widgetsBundle.getId().getId().getLeastSignificantBits()) - .setTitle(widgetsBundle.getTitle()) - .setAlias(widgetsBundle.getAlias()); - if (widgetsBundle.getImage() != null) { - builder.setImage(ByteString.copyFrom(widgetsBundle.getImage().getBytes(StandardCharsets.UTF_8))); - } - if (widgetsBundle.getDescription() != null) { - builder.setDescription(widgetsBundle.getDescription()); - } - if (widgetsBundle.getOrder() != null) { - builder.setOrder(widgetsBundle.getOrder()); - } - if (widgetsBundle.getTenantId().equals(TenantId.SYS_TENANT_ID)) { - builder.setIsSystem(true); - } - builder.setWidgets(JacksonUtil.toString(widgets)); - return builder.build(); - } - - public WidgetsBundleUpdateMsg constructWidgetsBundleDeleteMsg(WidgetsBundleId widgetsBundleId) { - return WidgetsBundleUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(widgetsBundleId.getId().getMostSignificantBits()) - .setIdLSB(widgetsBundleId.getId().getLeastSignificantBits()) - .build(); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java new file mode 100644 index 0000000000..fd3b4ef95d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.alarm; + +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface AlarmMsgConstructor extends MsgConstructor { + + AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorFactory.java new file mode 100644 index 0000000000..6d7e7ebd5c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.alarm; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class AlarmMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java index 15d7c27ff6..b1294f5904 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.alarm; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; @@ -24,10 +24,11 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class AlarmMsgConstructor { +public class AlarmMsgConstructorV1 implements AlarmMsgConstructor { + @Override public AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName) { - AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() + return AlarmUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(alarm.getId().getId().getMostSignificantBits()) .setIdLSB(alarm.getId().getId().getLeastSignificantBits()) @@ -44,8 +45,6 @@ public class AlarmMsgConstructor { .setDetails(JacksonUtil.toString(alarm.getDetails())) .setPropagate(alarm.isPropagate()) .setPropagateToOwner(alarm.isPropagateToOwner()) - .setPropagateToTenant(alarm.isPropagateToTenant()); - return builder.build(); + .setPropagateToTenant(alarm.isPropagateToTenant()).build(); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java new file mode 100644 index 0000000000..9034755db6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.alarm; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AlarmMsgConstructorV2 implements AlarmMsgConstructor { + + @Override + public AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName) { + return AlarmUpdateMsg.newBuilder().setMsgType(msgType) + .setEntity(JacksonUtil.toString(alarm)) + .setIdMSB(alarm.getId().getId().getMostSignificantBits()) + .setIdLSB(alarm.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructor.java new file mode 100644 index 0000000000..338375ac37 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructor.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.asset; + +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface AssetMsgConstructor extends MsgConstructor { + + AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset); + + AssetUpdateMsg constructAssetDeleteMsg(AssetId assetId); + + AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile); + + AssetProfileUpdateMsg constructAssetProfileDeleteMsg(AssetProfileId assetProfileId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorFactory.java new file mode 100644 index 0000000000..880beb446c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.asset; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class AssetMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java similarity index 63% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java index f5b921a32b..95e1e0e597 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.asset; import com.google.protobuf.ByteString; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -27,8 +29,34 @@ import java.nio.charset.StandardCharsets; @Component @TbCoreComponent -public class AssetProfileMsgConstructor { +public class AssetMsgConstructorV1 extends BaseAssetMsgConstructor { + @Override + public AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) { + AssetUpdateMsg.Builder builder = AssetUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(asset.getUuidId().getMostSignificantBits()) + .setIdLSB(asset.getUuidId().getLeastSignificantBits()) + .setName(asset.getName()) + .setType(asset.getType()); + if (asset.getLabel() != null) { + builder.setLabel(asset.getLabel()); + } + if (asset.getCustomerId() != null) { + builder.setCustomerIdMSB(asset.getCustomerId().getId().getMostSignificantBits()); + builder.setCustomerIdLSB(asset.getCustomerId().getId().getLeastSignificantBits()); + } + if (asset.getAssetProfileId() != null) { + builder.setAssetProfileIdMSB(asset.getAssetProfileId().getId().getMostSignificantBits()); + builder.setAssetProfileIdLSB(asset.getAssetProfileId().getId().getLeastSignificantBits()); + } + if (asset.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); + } + return builder.build(); + } + + @Override public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder() .setMsgType(msgType) @@ -55,12 +83,4 @@ public class AssetProfileMsgConstructor { } return builder.build(); } - - public AssetProfileUpdateMsg constructAssetProfileDeleteMsg(AssetProfileId assetProfileId) { - return AssetProfileUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(assetProfileId.getId().getMostSignificantBits()) - .setIdLSB(assetProfileId.getId().getLeastSignificantBits()).build(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV2.java new file mode 100644 index 0000000000..b7e00473b5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV2.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.asset; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AssetMsgConstructorV2 extends BaseAssetMsgConstructor { + + @Override + public AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) { + return AssetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(asset)) + .setIdMSB(asset.getUuidId().getMostSignificantBits()) + .setIdLSB(asset.getUuidId().getLeastSignificantBits()).build(); + } + + @Override + public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { + return AssetProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(assetProfile)) + .setIdMSB(assetProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/BaseAssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/BaseAssetMsgConstructor.java new file mode 100644 index 0000000000..b8fe8ad870 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/BaseAssetMsgConstructor.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.asset; + +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseAssetMsgConstructor implements AssetMsgConstructor { + + @Override + public AssetUpdateMsg constructAssetDeleteMsg(AssetId assetId) { + return AssetUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(assetId.getId().getMostSignificantBits()) + .setIdLSB(assetId.getId().getLeastSignificantBits()).build(); + } + + @Override + public AssetProfileUpdateMsg constructAssetProfileDeleteMsg(AssetProfileId assetProfileId) { + return AssetProfileUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(assetProfileId.getId().getMostSignificantBits()) + .setIdLSB(assetProfileId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/BaseCustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/BaseCustomerMsgConstructor.java new file mode 100644 index 0000000000..6d532a5e1c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/BaseCustomerMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.customer; + +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseCustomerMsgConstructor implements CustomerMsgConstructor { + + @Override + public CustomerUpdateMsg constructCustomerDeleteMsg(CustomerId customerId) { + return CustomerUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(customerId.getId().getMostSignificantBits()) + .setIdLSB(customerId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructor.java new file mode 100644 index 0000000000..7351822a3f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.customer; + +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface CustomerMsgConstructor extends MsgConstructor { + + CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer); + + CustomerUpdateMsg constructCustomerDeleteMsg(CustomerId customerId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorFactory.java new file mode 100644 index 0000000000..3e84fba57f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.customer; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class CustomerMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV1.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV1.java index 0465dd5a94..e6bb95defa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV1.java @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.customer; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class CustomerMsgConstructor { +public class CustomerMsgConstructorV1 extends BaseCustomerMsgConstructor { + @Override public CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { CustomerUpdateMsg.Builder builder = CustomerUpdateMsg.newBuilder() .setMsgType(msgType) @@ -62,11 +62,4 @@ public class CustomerMsgConstructor { } return builder.build(); } - - public CustomerUpdateMsg constructCustomerDeleteMsg(CustomerId customerId) { - return CustomerUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(customerId.getId().getMostSignificantBits()) - .setIdLSB(customerId.getId().getLeastSignificantBits()).build(); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV2.java new file mode 100644 index 0000000000..94764e1178 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/customer/CustomerMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.customer; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class CustomerMsgConstructorV2 extends BaseCustomerMsgConstructor { + + @Override + public CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { + return CustomerUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(customer)) + .setIdMSB(customer.getId().getId().getMostSignificantBits()) + .setIdLSB(customer.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java new file mode 100644 index 0000000000..f8e42c1461 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/BaseDashboardMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.dashboard; + +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseDashboardMsgConstructor implements DashboardMsgConstructor { + + @Override + public DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId) { + return DashboardUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(dashboardId.getId().getMostSignificantBits()) + .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java new file mode 100644 index 0000000000..96504e5036 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.dashboard; + +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface DashboardMsgConstructor extends MsgConstructor { + + DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard); + + DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorFactory.java new file mode 100644 index 0000000000..384327f5bc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.dashboard; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class DashboardMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java similarity index 79% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java index 50c2b46891..6a3bb43797 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.dashboard; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class DashboardMsgConstructor { +public class DashboardMsgConstructorV1 extends BaseDashboardMsgConstructor { + @Override public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) { DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder() .setMsgType(msgType) @@ -46,12 +46,4 @@ public class DashboardMsgConstructor { } return builder.build(); } - - public DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId) { - return DashboardUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(dashboardId.getId().getMostSignificantBits()) - .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java new file mode 100644 index 0000000000..0940423119 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.dashboard; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DashboardMsgConstructorV2 extends BaseDashboardMsgConstructor { + + @Override + public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) { + return DashboardUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(dashboard)) + .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) + .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/BaseDeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/BaseDeviceMsgConstructor.java new file mode 100644 index 0000000000..21d761dc26 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/BaseDeviceMsgConstructor.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.device; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; +import org.thingsboard.server.gen.edge.v1.RpcResponseMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.util.UUID; + +public abstract class BaseDeviceMsgConstructor implements DeviceMsgConstructor { + + @Override + public DeviceUpdateMsg constructDeviceDeleteMsg(DeviceId deviceId) { + return DeviceUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceId.getId().getMostSignificantBits()) + .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); + } + + @Override + public DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { + return DeviceProfileUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceProfileId.getId().getMostSignificantBits()) + .setIdLSB(deviceProfileId.getId().getLeastSignificantBits()).build(); + } + + @Override + public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { + DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); + if (body.has("error") || body.has("response")) { + RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder(); + if (body.has("error")) { + responseBuilder.setError(body.get("error").asText()); + } else { + responseBuilder.setResponse(body.get("response").asText()); + } + builder.setResponseMsg(responseBuilder.build()); + } else { + RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); + requestBuilder.setMethod(body.get("method").asText()); + requestBuilder.setParams(body.get("params").asText()); + builder.setRequestMsg(requestBuilder.build()); + } + return builder.build(); + } + + private DeviceRpcCallMsg.Builder constructDeviceRpcMsg(UUID deviceId, JsonNode body) { + DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() + .setDeviceIdMSB(deviceId.getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getLeastSignificantBits()) + .setRequestId(body.get("requestId").asInt()); + if (body.get("oneway") != null) { + builder.setOneway(body.get("oneway").asBoolean()); + } + if (body.get("requestUUID") != null) { + UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); + builder.setRequestUuidMSB(requestUUID.getMostSignificantBits()) + .setRequestUuidLSB(requestUUID.getLeastSignificantBits()); + } + if (body.get("expirationTime") != null) { + builder.setExpirationTime(body.get("expirationTime").asLong()); + } + if (body.get("persisted") != null) { + builder.setPersisted(body.get("persisted").asBoolean()); + } + if (body.get("retries") != null) { + builder.setRetries(body.get("retries").asInt()); + } + if (body.get("additionalInfo") != null) { + builder.setAdditionalInfo(JacksonUtil.toString(body.get("additionalInfo"))); + } + if (body.get("serviceId") != null) { + builder.setServiceId(body.get("serviceId").asText()); + } + if (body.get("sessionId") != null) { + builder.setSessionId(body.get("sessionId").asText()); + } + return builder; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java new file mode 100644 index 0000000000..4636dfd5ae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructor.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.device; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +import java.util.UUID; + +public interface DeviceMsgConstructor extends MsgConstructor { + + DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device); + + DeviceUpdateMsg constructDeviceDeleteMsg(DeviceId deviceId); + + DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials); + + DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile); + + DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId); + + DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorFactory.java new file mode 100644 index 0000000000..b384259e14 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorFactory.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.device; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class DeviceMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java similarity index 57% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java index 78b785a5d8..4442387b21 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java @@ -13,34 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.device; -import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; -import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; -import org.thingsboard.server.gen.edge.v1.RpcResponseMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; -import java.util.UUID; +import java.nio.charset.StandardCharsets; @Component @TbCoreComponent -public class DeviceMsgConstructor { +public class DeviceMsgConstructorV1 extends BaseDeviceMsgConstructor { @Autowired private DataDecodingEncodingService dataDecodingEncodingService; + @Override public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) { DeviceUpdateMsg.Builder builder = DeviceUpdateMsg.newBuilder() .setMsgType(msgType) @@ -76,6 +74,7 @@ public class DeviceMsgConstructor { return builder.build(); } + @Override public DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { DeviceCredentialsUpdateMsg.Builder builder = DeviceCredentialsUpdateMsg.newBuilder() .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits()) @@ -90,63 +89,50 @@ public class DeviceMsgConstructor { return builder.build(); } - public DeviceUpdateMsg constructDeviceDeleteMsg(DeviceId deviceId) { - return DeviceUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(deviceId.getId().getMostSignificantBits()) - .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); - } - - public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { - DeviceRpcCallMsg.Builder builder = constructDeviceRpcMsg(deviceId, body); - if (body.has("error") || body.has("response")) { - RpcResponseMsg.Builder responseBuilder = RpcResponseMsg.newBuilder(); - if (body.has("error")) { - responseBuilder.setError(body.get("error").asText()); - } else { - responseBuilder.setResponse(body.get("response").asText()); - } - builder.setResponseMsg(responseBuilder.build()); - } else { - RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); - requestBuilder.setMethod(body.get("method").asText()); - requestBuilder.setParams(body.get("params").asText()); - builder.setRequestMsg(requestBuilder.build()); + @Override + public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { + DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + .setName(deviceProfile.getName()) + .setDefault(deviceProfile.isDefault()) + .setType(deviceProfile.getType().name()) + .setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile.getProfileData()))); + if (deviceProfile.getDefaultQueueName() != null) { + builder.setDefaultQueueName(deviceProfile.getDefaultQueueName()); } - return builder.build(); - } - - private DeviceRpcCallMsg.Builder constructDeviceRpcMsg(UUID deviceId, JsonNode body) { - DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() - .setDeviceIdMSB(deviceId.getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getLeastSignificantBits()) - .setRequestId(body.get("requestId").asInt()); - if (body.get("oneway") != null) { - builder.setOneway(body.get("oneway").asBoolean()); + if (deviceProfile.getDescription() != null) { + builder.setDescription(deviceProfile.getDescription()); } - if (body.get("requestUUID") != null) { - UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); - builder.setRequestUuidMSB(requestUUID.getMostSignificantBits()) - .setRequestUuidLSB(requestUUID.getLeastSignificantBits()); + if (deviceProfile.getTransportType() != null) { + builder.setTransportType(deviceProfile.getTransportType().name()); } - if (body.get("expirationTime") != null) { - builder.setExpirationTime(body.get("expirationTime").asLong()); + if (deviceProfile.getProvisionType() != null) { + builder.setProvisionType(deviceProfile.getProvisionType().name()); } - if (body.get("persisted") != null) { - builder.setPersisted(body.get("persisted").asBoolean()); + if (deviceProfile.getProvisionDeviceKey() != null) { + builder.setProvisionDeviceKey(deviceProfile.getProvisionDeviceKey()); } - if (body.get("retries") != null) { - builder.setRetries(body.get("retries").asInt()); + if (deviceProfile.getImage() != null) { + builder.setImage(ByteString.copyFrom(deviceProfile.getImage().getBytes(StandardCharsets.UTF_8))); } - if (body.get("additionalInfo") != null) { - builder.setAdditionalInfo(JacksonUtil.toString(body.get("additionalInfo"))); + if (deviceProfile.getFirmwareId() != null) { + builder.setFirmwareIdMSB(deviceProfile.getFirmwareId().getId().getMostSignificantBits()) + .setFirmwareIdLSB(deviceProfile.getFirmwareId().getId().getLeastSignificantBits()); } - if (body.get("serviceId") != null) { - builder.setServiceId(body.get("serviceId").asText()); + if (deviceProfile.getSoftwareId() != null) { + builder.setSoftwareIdMSB(deviceProfile.getSoftwareId().getId().getMostSignificantBits()) + .setSoftwareIdLSB(deviceProfile.getSoftwareId().getId().getLeastSignificantBits()); } - if (body.get("sessionId") != null) { - builder.setSessionId(body.get("sessionId").asText()); + if (deviceProfile.getDefaultEdgeRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); } - return builder; + if (deviceProfile.getDefaultDashboardId() != null) { + builder.setDefaultDashboardIdMSB(deviceProfile.getDefaultDashboardId().getId().getMostSignificantBits()) + .setDefaultDashboardIdLSB(deviceProfile.getDefaultDashboardId().getId().getLeastSignificantBits()); + } + return builder.build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java new file mode 100644 index 0000000000..189251f8c4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV2.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.device; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DeviceMsgConstructorV2 extends BaseDeviceMsgConstructor { + + @Override + public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) { + return DeviceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(device)) + .setIdMSB(device.getId().getId().getMostSignificantBits()) + .setIdLSB(device.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { + return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build(); + } + + @Override + public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { + return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(deviceProfile)) + .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java index 3416240b2c..c3741ee8bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EdgeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/edge/EdgeMsgConstructor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.edge; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java new file mode 100644 index 0000000000..a037e3ee09 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/BaseEntityViewMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.entityview; + +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseEntityViewMsgConstructor implements EntityViewMsgConstructor { + + @Override + public EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId) { + return EntityViewUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(entityViewId.getId().getMostSignificantBits()) + .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java new file mode 100644 index 0000000000..7f14f62c68 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.entityview; + +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface EntityViewMsgConstructor extends MsgConstructor { + + EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView); + + EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorFactory.java new file mode 100644 index 0000000000..849f7f093a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.entityview; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class EntityViewMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java similarity index 72% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java index 235199d75e..da369ecf90 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV1.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.entityview; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.gen.edge.v1.EdgeEntityType; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -26,20 +26,11 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class EntityViewMsgConstructor { +public class EntityViewMsgConstructorV1 extends BaseEntityViewMsgConstructor { + @Override public EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) { - EdgeEntityType entityType; - switch (entityView.getEntityId().getEntityType()) { - case DEVICE: - entityType = EdgeEntityType.DEVICE; - break; - case ASSET: - entityType = EdgeEntityType.ASSET; - break; - default: - throw new RuntimeException("Unsupported entity type [" + entityView.getEntityId().getEntityType() + "]"); - } + EdgeEntityType edgeEntityType = checkEntityType(entityView.getEntityId().getEntityType()); EntityViewUpdateMsg.Builder builder = EntityViewUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(entityView.getId().getId().getMostSignificantBits()) @@ -48,7 +39,7 @@ public class EntityViewMsgConstructor { .setType(entityView.getType()) .setEntityIdMSB(entityView.getEntityId().getId().getMostSignificantBits()) .setEntityIdLSB(entityView.getEntityId().getId().getLeastSignificantBits()) - .setEntityType(entityType); + .setEntityType(edgeEntityType); if (entityView.getCustomerId() != null) { builder.setCustomerIdMSB(entityView.getCustomerId().getId().getMostSignificantBits()); builder.setCustomerIdLSB(entityView.getCustomerId().getId().getLeastSignificantBits()); @@ -59,10 +50,14 @@ public class EntityViewMsgConstructor { return builder.build(); } - public EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId) { - return EntityViewUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(entityViewId.getId().getMostSignificantBits()) - .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); + private EdgeEntityType checkEntityType(EntityType entityType) { + switch (entityType) { + case DEVICE: + return EdgeEntityType.DEVICE; + case ASSET: + return EdgeEntityType.ASSET; + default: + throw new RuntimeException("Unsupported entity type [" + entityType + "]"); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java new file mode 100644 index 0000000000..c1eae91097 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/entityview/EntityViewMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.entityview; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class EntityViewMsgConstructorV2 extends BaseEntityViewMsgConstructor { + + @Override + public EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) { + return EntityViewUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityView)) + .setIdMSB(entityView.getId().getId().getMostSignificantBits()) + .setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/BaseOtaPackageMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/BaseOtaPackageMsgConstructor.java new file mode 100644 index 0000000000..110c24caca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/BaseOtaPackageMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.ota; + +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseOtaPackageMsgConstructor implements OtaPackageMsgConstructor { + + @Override + public OtaPackageUpdateMsg constructOtaPackageDeleteMsg(OtaPackageId otaPackageId) { + return OtaPackageUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(otaPackageId.getId().getMostSignificantBits()) + .setIdLSB(otaPackageId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructor.java new file mode 100644 index 0000000000..500500a7fc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.ota; + +import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface OtaPackageMsgConstructor extends MsgConstructor { + + OtaPackageUpdateMsg constructOtaPackageUpdatedMsg(UpdateMsgType msgType, OtaPackage otaPackage); + + OtaPackageUpdateMsg constructOtaPackageDeleteMsg(OtaPackageId otaPackageId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorFactory.java new file mode 100644 index 0000000000..fd775d77ff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.ota; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV2; + +@Component +@TbCoreComponent +public class OtaPackageMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV1.java similarity index 84% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV1.java index 253305a5ab..5e1f297c01 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/OtaPackageMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV1.java @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.ota; import com.google.protobuf.ByteString; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.OtaPackage; -import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class OtaPackageMsgConstructor { +public class OtaPackageMsgConstructorV1 extends BaseOtaPackageMsgConstructor { + @Override public OtaPackageUpdateMsg constructOtaPackageUpdatedMsg(UpdateMsgType msgType, OtaPackage otaPackage) { OtaPackageUpdateMsg.Builder builder = OtaPackageUpdateMsg.newBuilder() .setMsgType(msgType) @@ -69,12 +69,4 @@ public class OtaPackageMsgConstructor { } return builder.build(); } - - public OtaPackageUpdateMsg constructOtaPackageDeleteMsg(OtaPackageId otaPackageId) { - return OtaPackageUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(otaPackageId.getId().getMostSignificantBits()) - .setIdLSB(otaPackageId.getId().getLeastSignificantBits()).build(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV2.java new file mode 100644 index 0000000000..69d7f12874 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ota/OtaPackageMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.ota; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class OtaPackageMsgConstructorV2 extends BaseOtaPackageMsgConstructor { + + @Override + public OtaPackageUpdateMsg constructOtaPackageUpdatedMsg(UpdateMsgType msgType, OtaPackage otaPackage) { + return OtaPackageUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(otaPackage)) + .setIdMSB(otaPackage.getId().getId().getMostSignificantBits()) + .setIdLSB(otaPackage.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/BaseQueueMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/BaseQueueMsgConstructor.java new file mode 100644 index 0000000000..486a143d85 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/BaseQueueMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.queue; + +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseQueueMsgConstructor implements QueueMsgConstructor { + + @Override + public QueueUpdateMsg constructQueueDeleteMsg(QueueId queueId) { + return QueueUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(queueId.getId().getMostSignificantBits()) + .setIdLSB(queueId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructor.java new file mode 100644 index 0000000000..847f19419e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.queue; + +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface QueueMsgConstructor extends MsgConstructor { + + QueueUpdateMsg constructQueueUpdatedMsg(UpdateMsgType msgType, Queue queue); + + QueueUpdateMsg constructQueueDeleteMsg(QueueId queueId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorFactory.java new file mode 100644 index 0000000000..0f9a4b9e2e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.queue; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class QueueMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV1.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV1.java index 2644a5409b..8621ce1011 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/QueueMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV1.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.queue; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.QueueId; @@ -28,10 +28,11 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class QueueMsgConstructor { +public class QueueMsgConstructorV1 extends BaseQueueMsgConstructor { + @Override public QueueUpdateMsg constructQueueUpdatedMsg(UpdateMsgType msgType, Queue queue) { - QueueUpdateMsg.Builder builder = QueueUpdateMsg.newBuilder() + return QueueUpdateMsg.newBuilder() .setMsgType(msgType) .setIdMSB(queue.getId().getId().getMostSignificantBits()) .setIdLSB(queue.getId().getId().getLeastSignificantBits()) @@ -44,8 +45,7 @@ public class QueueMsgConstructor { .setConsumerPerPartition(queue.isConsumerPerPartition()) .setPackProcessingTimeout(queue.getPackProcessingTimeout()) .setSubmitStrategy(createSubmitStrategyProto(queue.getSubmitStrategy())) - .setProcessingStrategy(createProcessingStrategyProto(queue.getProcessingStrategy())); - return builder.build(); + .setProcessingStrategy(createProcessingStrategyProto(queue.getProcessingStrategy())).build(); } private ProcessingStrategyProto createProcessingStrategyProto(ProcessingStrategy processingStrategy) { @@ -71,5 +71,4 @@ public class QueueMsgConstructor { .setIdMSB(queueId.getId().getMostSignificantBits()) .setIdLSB(queueId.getId().getLeastSignificantBits()).build(); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV2.java new file mode 100644 index 0000000000..1bf80eea64 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/queue/QueueMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.queue; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class QueueMsgConstructorV2 extends BaseQueueMsgConstructor { + + @Override + public QueueUpdateMsg constructQueueUpdatedMsg(UpdateMsgType msgType, Queue queue) { + return QueueUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(queue)) + .setIdMSB(queue.getId().getId().getMostSignificantBits()) + .setIdLSB(queue.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructor.java new file mode 100644 index 0000000000..f7fac6ce71 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructor.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.relation; + +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface RelationMsgConstructor extends MsgConstructor { + + RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorFactory.java new file mode 100644 index 0000000000..4ec02c5b49 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.relation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV2; + +@Component +@TbCoreComponent +public class RelationMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV1.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV1.java index 24786a4f7c..394086fdd0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV1.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.relation; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; @@ -24,8 +24,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class RelationMsgConstructor { +public class RelationMsgConstructorV1 implements RelationMsgConstructor { + @Override public RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { RelationUpdateMsg.Builder builder = RelationUpdateMsg.newBuilder() .setMsgType(msgType) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV2.java new file mode 100644 index 0000000000..4510909490 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/relation/RelationMsgConstructorV2.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.relation; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class RelationMsgConstructorV2 implements RelationMsgConstructor { + + + @Override + public RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { + return RelationUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityRelation)).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/BaseResourceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/BaseResourceMsgConstructor.java new file mode 100644 index 0000000000..620f71c9e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/BaseResourceMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.resource; + +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +public abstract class BaseResourceMsgConstructor implements ResourceMsgConstructor { + + @Override + public ResourceUpdateMsg constructResourceDeleteMsg(TbResourceId tbResourceId) { + return ResourceUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(tbResourceId.getId().getMostSignificantBits()) + .setIdLSB(tbResourceId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructor.java new file mode 100644 index 0000000000..cfeedced80 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.resource; + +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface ResourceMsgConstructor extends MsgConstructor { + + ResourceUpdateMsg constructResourceUpdatedMsg(UpdateMsgType msgType, TbResource tbResource); + + ResourceUpdateMsg constructResourceDeleteMsg(TbResourceId tbResourceId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorFactory.java new file mode 100644 index 0000000000..2c67d8731e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.resource; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class ResourceMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ResourceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV1.java similarity index 74% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ResourceMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV1.java index 27b7dc3978..d110076bea 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/ResourceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV1.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.resource; +import com.google.protobuf.ByteString; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -25,8 +25,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class ResourceMsgConstructor { +public class ResourceMsgConstructorV1 extends BaseResourceMsgConstructor { + @Override public ResourceUpdateMsg constructResourceUpdatedMsg(UpdateMsgType msgType, TbResource tbResource) { ResourceUpdateMsg.Builder builder = ResourceUpdateMsg.newBuilder() .setMsgType(msgType) @@ -37,21 +38,14 @@ public class ResourceMsgConstructor { .setResourceType(tbResource.getResourceType().name()) .setFileName(tbResource.getFileName()); if (tbResource.getData() != null) { - builder.setData(tbResource.getData()); + builder.setData(tbResource.getEncodedData()); } if (tbResource.getEtag() != null) { builder.setEtag(tbResource.getEtag()); } - if (tbResource.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + if (TenantId.SYS_TENANT_ID.equals(tbResource.getTenantId())) { builder.setIsSystem(true); } return builder.build(); } - - public ResourceUpdateMsg constructResourceDeleteMsg(TbResourceId tbResourceId) { - return ResourceUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(tbResourceId.getId().getMostSignificantBits()) - .setIdLSB(tbResourceId.getId().getLeastSignificantBits()).build(); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV2.java new file mode 100644 index 0000000000..094e5636bb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV2.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.resource; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class ResourceMsgConstructorV2 extends BaseResourceMsgConstructor { + + @Override + public ResourceUpdateMsg constructResourceUpdatedMsg(UpdateMsgType msgType, TbResource tbResource) { + return ResourceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tbResource)) + .setIdMSB(tbResource.getId().getId().getMostSignificantBits()) + .setIdLSB(tbResource.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java similarity index 88% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java index a720d0aab6..be10a714f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; @@ -36,23 +37,17 @@ import java.util.NavigableSet; @Slf4j @AllArgsConstructor -public abstract class AbstractRuleChainMetadataConstructor implements RuleChainMetadataConstructor { +public abstract class BaseRuleChainMetadataConstructor implements RuleChainMetadataConstructor { @Override public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, - RuleChainMetaData ruleChainMetaData) { - try { - RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder(); - builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) - .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()); - constructRuleChainMetadataUpdatedMsg(tenantId, builder, ruleChainMetaData); - builder.setMsgType(msgType); - return builder.build(); - } catch (Exception ex) { - log.error("[{}] Can't construct RuleChainMetadataUpdateMsg", tenantId, ex); - } - return null; + RuleChainMetaData ruleChainMetaData, + EdgeVersion edgeVersion) { + RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder(); + constructRuleChainMetadataUpdatedMsg(tenantId, builder, ruleChainMetaData); + builder.setMsgType(msgType); + return builder.build(); } protected abstract void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMsgConstructor.java similarity index 54% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMsgConstructor.java index 8f91f5d19e..cf5967fac1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMsgConstructor.java @@ -13,57 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.rule; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMetadataConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMetadataConstructorFactory; -@Component -@Slf4j -@TbCoreComponent -public class RuleChainMsgConstructor { +public abstract class BaseRuleChainMsgConstructor implements RuleChainMsgConstructor { - public RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { - RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() - .setMsgType(msgType) - .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) - .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()) - .setName(ruleChain.getName()) - .setRoot(isRoot) - .setDebugMode(ruleChain.isDebugMode()) - .setConfiguration(JacksonUtil.toString(ruleChain.getConfiguration())); - if (ruleChain.getFirstRuleNodeId() != null) { - builder.setFirstRuleNodeIdMSB(ruleChain.getFirstRuleNodeId().getId().getMostSignificantBits()) - .setFirstRuleNodeIdLSB(ruleChain.getFirstRuleNodeId().getId().getLeastSignificantBits()); - } - return builder.build(); + @Override + public RuleChainUpdateMsg constructRuleChainDeleteMsg(RuleChainId ruleChainId) { + return RuleChainUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); } + @Override public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - RuleChainMetadataConstructor ruleChainMetadataConstructor - = RuleChainMetadataConstructorFactory.getByEdgeVersion(edgeVersion); - return ruleChainMetadataConstructor.constructRuleChainMetadataUpdatedMsg(tenantId, msgType, ruleChainMetaData); - } - - public RuleChainUpdateMsg constructRuleChainDeleteMsg(RuleChainId ruleChainId) { - return RuleChainUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(ruleChainId.getId().getMostSignificantBits()) - .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); + RuleChainMetadataConstructor ruleChainMetadataConstructor = RuleChainMetadataConstructorFactory.getByEdgeVersion(edgeVersion); + return ruleChainMetadataConstructor.constructRuleChainMetadataUpdatedMsg(tenantId, msgType, ruleChainMetaData, edgeVersion); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java index 51b7380e48..f80b30b3c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructor.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge.rpc.constructor.rule; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -24,5 +25,6 @@ public interface RuleChainMetadataConstructor { RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, - RuleChainMetaData ruleChainMetaData); + RuleChainMetaData ruleChainMetaData, + EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java index 1c581267c4..cdc7e128d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorFactory.java @@ -25,8 +25,12 @@ public final class RuleChainMetadataConstructorFactory { return new RuleChainMetadataConstructorV330(); case V_3_3_3: case V_3_4_0: - default: + case V_3_6_0: + case V_3_6_1: return new RuleChainMetadataConstructorV340(); + case V_3_6_2: + default: + return new RuleChainMetadataConstructorV362(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java index 33fc39b7e7..64d362342b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV330.java @@ -36,7 +36,7 @@ import java.util.UUID; import java.util.stream.Collectors; @Slf4j -public class RuleChainMetadataConstructorV330 extends AbstractRuleChainMetadataConstructor { +public class RuleChainMetadataConstructorV330 extends BaseRuleChainMetadataConstructor { private static final String RULE_CHAIN_INPUT_NODE = TbRuleChainInputNode.class.getName(); private static final String TB_RULE_CHAIN_OUTPUT_NODE = TbRuleChainOutputNode.class.getName(); @@ -45,6 +45,8 @@ public class RuleChainMetadataConstructorV330 extends AbstractRuleChainMetadataC protected void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, RuleChainMetadataUpdateMsg.Builder builder, RuleChainMetaData ruleChainMetaData) { + builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()); List supportedNodes = filterNodes(ruleChainMetaData.getNodes()); NavigableSet removedNodeIndexes = getRemovedNodeIndexes(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java index 9a37e416db..2133024931 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV340.java @@ -23,12 +23,14 @@ import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import java.util.TreeSet; @Slf4j -public class RuleChainMetadataConstructorV340 extends AbstractRuleChainMetadataConstructor { +public class RuleChainMetadataConstructorV340 extends BaseRuleChainMetadataConstructor { @Override protected void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, RuleChainMetadataUpdateMsg.Builder builder, RuleChainMetaData ruleChainMetaData) { + builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()); builder.addAllNodes(constructNodes(ruleChainMetaData.getNodes())) .addAllConnections(constructConnections(ruleChainMetaData.getConnections())) .addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections(), new TreeSet<>())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV362.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV362.java new file mode 100644 index 0000000000..7a8a6d02cb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMetadataConstructorV362.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.rule; + +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; + +public class RuleChainMetadataConstructorV362 extends BaseRuleChainMetadataConstructor { + + @Override + protected void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, RuleChainMetadataUpdateMsg.Builder builder, RuleChainMetaData ruleChainMetaData) { + builder.setEntity(JacksonUtil.toString(ruleChainMetaData)); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructor.java new file mode 100644 index 0000000000..8ea2ff9a63 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructor.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.rule; + +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface RuleChainMsgConstructor extends MsgConstructor { + + RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot); + + RuleChainUpdateMsg constructRuleChainDeleteMsg(RuleChainId ruleChainId); + + RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, + UpdateMsgType msgType, + RuleChainMetaData ruleChainMetaData, + EdgeVersion edgeVersion); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorFactory.java new file mode 100644 index 0000000000..2c1a839ab9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.rule; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class RuleChainMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV1.java new file mode 100644 index 0000000000..6c918cfc56 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV1.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.rule; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class RuleChainMsgConstructorV1 extends BaseRuleChainMsgConstructor { + + @Override + public RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { + RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) + .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()) + .setName(ruleChain.getName()) + .setRoot(isRoot) + .setDebugMode(ruleChain.isDebugMode()) + .setConfiguration(JacksonUtil.toString(ruleChain.getConfiguration())); + if (ruleChain.getFirstRuleNodeId() != null) { + builder.setFirstRuleNodeIdMSB(ruleChain.getFirstRuleNodeId().getId().getMostSignificantBits()) + .setFirstRuleNodeIdLSB(ruleChain.getFirstRuleNodeId().getId().getLeastSignificantBits()); + } + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV2.java new file mode 100644 index 0000000000..f5c1c12b39 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/RuleChainMsgConstructorV2.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.rule; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class RuleChainMsgConstructorV2 extends BaseRuleChainMsgConstructor { + + @Override + public RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) { + boolean isTemplateRoot = ruleChain.isRoot(); + ruleChain.setRoot(isRoot); + RuleChainUpdateMsg result = RuleChainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChain)) + .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) + .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()).build(); + ruleChain.setRoot(isTemplateRoot); + return result; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructor.java new file mode 100644 index 0000000000..da4f964218 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructor.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.settings; + +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface AdminSettingsMsgConstructor extends MsgConstructor { + + AdminSettingsUpdateMsg constructAdminSettingsUpdateMsg(AdminSettings adminSettings); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorFactory.java new file mode 100644 index 0000000000..ecf6131e52 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.settings; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class AdminSettingsMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV1.java similarity index 81% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV1.java index 79754b867c..cbbd9b5bb8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV1.java @@ -13,26 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.settings; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class AdminSettingsMsgConstructor { +public class AdminSettingsMsgConstructorV1 implements AdminSettingsMsgConstructor { + @Override public AdminSettingsUpdateMsg constructAdminSettingsUpdateMsg(AdminSettings adminSettings) { AdminSettingsUpdateMsg.Builder builder = AdminSettingsUpdateMsg.newBuilder() .setKey(adminSettings.getKey()) .setJsonValue(JacksonUtil.toString(adminSettings.getJsonValue())); - if (adminSettings.getId() != null) { + if (TenantId.SYS_TENANT_ID.equals(adminSettings.getTenantId())) { builder.setIsSystem(true); } return builder.build(); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV2.java new file mode 100644 index 0000000000..de92d2a1f9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/settings/AdminSettingsMsgConstructorV2.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.settings; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AdminSettingsMsgConstructorV2 implements AdminSettingsMsgConstructor { + + @Override + public AdminSettingsUpdateMsg constructAdminSettingsUpdateMsg(AdminSettings adminSettings) { + return AdminSettingsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(adminSettings)).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/telemetry/EntityDataMsgConstructor.java similarity index 97% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/telemetry/EntityDataMsgConstructor.java index 47dbad231d..4ec6376a4d 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/telemetry/EntityDataMsgConstructor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.telemetry; import com.google.gson.Gson; import com.google.gson.JsonArray; @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.transport.TransportProtos; @@ -36,8 +36,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.List; -@Component @Slf4j +@Component @TbCoreComponent public class EntityDataMsgConstructor { @@ -106,5 +106,4 @@ public class EntityDataMsgConstructor { } return result; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructor.java new file mode 100644 index 0000000000..3de654fda6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.tenant; + +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface TenantMsgConstructor extends MsgConstructor { + + TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant); + + TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorFactory.java new file mode 100644 index 0000000000..7735d67684 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.tenant; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class TenantMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java similarity index 59% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java index 321a57970a..a3f815cc18 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java @@ -13,19 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.tenant; +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @Component @TbCoreComponent -public class TenantMsgConstructor { +public class TenantMsgConstructorV1 implements TenantMsgConstructor { + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + @Override public TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) { TenantUpdateMsg.Builder builder = TenantUpdateMsg.newBuilder() .setMsgType(msgType) @@ -64,4 +75,22 @@ public class TenantMsgConstructor { } return builder.build(); } + + @Override + public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) { + ByteString profileData = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_1) ? + ByteString.empty() : ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData())); + TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(tenantProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(tenantProfile.getId().getId().getLeastSignificantBits()) + .setName(tenantProfile.getName()) + .setDefault(tenantProfile.isDefault()) + .setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine()) + .setProfileDataBytes(profileData); + if (tenantProfile.getDescription() != null) { + builder.setDescription(tenantProfile.getDescription()); + } + return builder.build(); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV2.java new file mode 100644 index 0000000000..69531e5630 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV2.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.tenant; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class TenantMsgConstructorV2 implements TenantMsgConstructor { + + @Override + public TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) { + return TenantUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenant)).build(); + } + + @Override + public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) { + return TenantProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenantProfile)).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/BaseUserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/BaseUserMsgConstructor.java new file mode 100644 index 0000000000..aef35d8f50 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/BaseUserMsgConstructor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.user; + +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; + +public abstract class BaseUserMsgConstructor implements UserMsgConstructor { + + @Override + public UserUpdateMsg constructUserDeleteMsg(UserId userId) { + return UserUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(userId.getId().getMostSignificantBits()) + .setIdLSB(userId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructor.java new file mode 100644 index 0000000000..3184f8c751 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructor.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.user; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +public interface UserMsgConstructor extends MsgConstructor { + + UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user); + + UserUpdateMsg constructUserDeleteMsg(UserId userId); + + UserCredentialsUpdateMsg constructUserCredentialsUpdatedMsg(UserCredentials userCredentials); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorFactory.java new file mode 100644 index 0000000000..bf1e4197c0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.user; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class UserMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV1.java similarity index 79% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV1.java index fbd3b31688..3fe635c54c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV1.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.user; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; @@ -27,8 +26,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent -public class UserMsgConstructor { +public class UserMsgConstructorV1 extends BaseUserMsgConstructor { + @Override public UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) { UserUpdateMsg.Builder builder = UserUpdateMsg.newBuilder() .setMsgType(msgType) @@ -52,19 +52,12 @@ public class UserMsgConstructor { return builder.build(); } - public UserUpdateMsg constructUserDeleteMsg(UserId userId) { - return UserUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(userId.getId().getMostSignificantBits()) - .setIdLSB(userId.getId().getLeastSignificantBits()).build(); - } - + @Override public UserCredentialsUpdateMsg constructUserCredentialsUpdatedMsg(UserCredentials userCredentials) { - UserCredentialsUpdateMsg.Builder builder = UserCredentialsUpdateMsg.newBuilder() + return UserCredentialsUpdateMsg.newBuilder() .setUserIdMSB(userCredentials.getUserId().getId().getMostSignificantBits()) .setUserIdLSB(userCredentials.getUserId().getId().getLeastSignificantBits()) .setEnabled(userCredentials.isEnabled()) - .setPassword(userCredentials.getPassword()); - return builder.build(); + .setPassword(userCredentials.getPassword()).build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV2.java new file mode 100644 index 0000000000..0b4c592712 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/user/UserMsgConstructorV2.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.user; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class UserMsgConstructorV2 extends BaseUserMsgConstructor { + + @Override + public UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) { + return UserUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(user)) + .setIdMSB(user.getId().getId().getMostSignificantBits()) + .setIdLSB(user.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public UserCredentialsUpdateMsg constructUserCredentialsUpdatedMsg(UserCredentials userCredentials) { + return UserCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(userCredentials)).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/BaseWidgetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/BaseWidgetMsgConstructor.java new file mode 100644 index 0000000000..6b4b3ac821 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/BaseWidgetMsgConstructor.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.widget; + +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; + +public abstract class BaseWidgetMsgConstructor implements WidgetMsgConstructor { + + @Override + public WidgetsBundleUpdateMsg constructWidgetsBundleDeleteMsg(WidgetsBundleId widgetsBundleId) { + return WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetsBundleId.getId().getMostSignificantBits()) + .setIdLSB(widgetsBundleId.getId().getLeastSignificantBits()) + .build(); + } + + @Override + public WidgetTypeUpdateMsg constructWidgetTypeDeleteMsg(WidgetTypeId widgetTypeId) { + return WidgetTypeUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetTypeId.getId().getMostSignificantBits()) + .setIdLSB(widgetTypeId.getId().getLeastSignificantBits()) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructor.java new file mode 100644 index 0000000000..544ba39b13 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructor.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.widget; + +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor; + +import java.util.List; + +public interface WidgetMsgConstructor extends MsgConstructor { + + WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets); + + WidgetsBundleUpdateMsg constructWidgetsBundleDeleteMsg(WidgetsBundleId widgetsBundleId); + + WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion); + + WidgetTypeUpdateMsg constructWidgetTypeDeleteMsg(WidgetTypeId widgetTypeId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorFactory.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorFactory.java new file mode 100644 index 0000000000..3a963f7041 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.widget; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructorFactory; + +@Component +@TbCoreComponent +public class WidgetMsgConstructorFactory extends MsgConstructorFactory { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java similarity index 67% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java index 17a66a47d4..67bdbf1f54 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java @@ -13,25 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.constructor; +package org.thingsboard.server.service.edge.rpc.constructor.widget; +import com.google.protobuf.ByteString; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; @Component @TbCoreComponent -public class WidgetTypeMsgConstructor { +public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor { + @Override + public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets) { + WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetsBundle.getId().getId().getLeastSignificantBits()) + .setTitle(widgetsBundle.getTitle()) + .setAlias(widgetsBundle.getAlias()); + if (widgetsBundle.getImage() != null) { + builder.setImage(ByteString.copyFrom(widgetsBundle.getImage().getBytes(StandardCharsets.UTF_8))); + } + if (widgetsBundle.getDescription() != null) { + builder.setDescription(widgetsBundle.getDescription()); + } + if (widgetsBundle.getOrder() != null) { + builder.setOrder(widgetsBundle.getOrder()); + } + if (widgetsBundle.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + builder.setIsSystem(true); + } + builder.setWidgets(JacksonUtil.toString(widgets)); + return builder.build(); + } + + @Override public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) { WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() .setMsgType(msgType) @@ -73,12 +102,4 @@ public class WidgetTypeMsgConstructor { } return builder.build(); } - - public WidgetTypeUpdateMsg constructWidgetTypeDeleteMsg(WidgetTypeId widgetTypeId) { - return WidgetTypeUpdateMsg.newBuilder() - .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) - .setIdMSB(widgetTypeId.getId().getMostSignificantBits()) - .setIdLSB(widgetTypeId.getId().getLeastSignificantBits()) - .build(); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV2.java new file mode 100644 index 0000000000..aacc3c8399 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV2.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor.widget; + +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.List; + +@Component +@TbCoreComponent +public class WidgetMsgConstructorV2 extends BaseWidgetMsgConstructor { + + @Override + public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List widgets) { + return WidgetsBundleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetsBundle)) + .setWidgets(JacksonUtil.toString(widgets)) + .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetsBundle.getId().getId().getLeastSignificantBits()).build(); + } + + @Override + public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) { + return WidgetTypeUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetTypeDetails)) + .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 8790b5d1fd..fe5fc76f2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -90,27 +90,23 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.ResourceMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.telemetry.EntityDataMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorFactory; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -253,61 +249,49 @@ public abstract class BaseEdgeProcessor { protected EntityDataMsgConstructor entityDataMsgConstructor; @Autowired - protected RuleChainMsgConstructor ruleChainMsgConstructor; + protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; @Autowired - protected AlarmMsgConstructor alarmMsgConstructor; + protected AlarmMsgConstructorFactory alarmMsgConstructorFactory; @Autowired - protected DeviceMsgConstructor deviceMsgConstructor; + protected DeviceMsgConstructorFactory deviceMsgConstructorFactory; @Autowired - protected AssetMsgConstructor assetMsgConstructor; + protected AssetMsgConstructorFactory assetMsgConstructorFactory; @Autowired - protected EntityViewMsgConstructor entityViewMsgConstructor; + protected EntityViewMsgConstructorFactory entityViewMsgConstructorFactory; @Autowired - protected DashboardMsgConstructor dashboardMsgConstructor; + protected DashboardMsgConstructorFactory dashboardMsgConstructorFactory; @Autowired - protected RelationMsgConstructor relationMsgConstructor; + protected RelationMsgConstructorFactory relationMsgConstructorFactory; @Autowired - protected UserMsgConstructor userMsgConstructor; + protected UserMsgConstructorFactory userMsgConstructorFactory; @Autowired - protected CustomerMsgConstructor customerMsgConstructor; + protected CustomerMsgConstructorFactory customerMsgConstructorFactory; @Autowired - protected DeviceProfileMsgConstructor deviceProfileMsgConstructor; + protected TenantMsgConstructorFactory tenantMsgConstructorFactory; @Autowired - protected AssetProfileMsgConstructor assetProfileMsgConstructor; + protected WidgetMsgConstructorFactory widgetMsgConstructorFactory; @Autowired - protected TenantMsgConstructor tenantMsgConstructor; + protected AdminSettingsMsgConstructorFactory adminSettingsMsgConstructorFactory; @Autowired - protected TenantProfileMsgConstructor tenantProfileMsgConstructor; + protected OtaPackageMsgConstructorFactory otaPackageMsgConstructorFactory; @Autowired - protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; + protected QueueMsgConstructorFactory queueMsgConstructorFactory; @Autowired - protected WidgetTypeMsgConstructor widgetTypeMsgConstructor; - - @Autowired - protected AdminSettingsMsgConstructor adminSettingsMsgConstructor; - - @Autowired - protected OtaPackageMsgConstructor otaPackageMsgConstructor; - - @Autowired - protected QueueMsgConstructor queueMsgConstructor; - - @Autowired - protected ResourceMsgConstructor resourceMsgConstructor; + protected ResourceMsgConstructorFactory resourceMsgConstructorFactory; @Autowired protected EdgeSynchronizationManager edgeSynchronizationManager; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index 882fd90c46..b934a33d54 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -46,19 +47,19 @@ import java.util.UUID; @TbCoreComponent public class AlarmEdgeProcessor extends BaseAlarmProcessor { - public ListenableFuture processAlarmMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmUpdateMsg alarmUpdateMsg) { + public ListenableFuture processAlarmMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmUpdateMsg alarmUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] processAlarmMsgFromEdge [{}]", tenantId, alarmUpdateMsg); try { edgeSynchronizationManager.getEdgeId().set(edgeId); - return processAlarmMsg(tenantId, alarmUpdateMsg); + return processAlarmMsg(tenantId, alarmUpdateMsg, edgeVersion); } finally { edgeSynchronizationManager.getEdgeId().remove(); } } - public DownlinkMsg convertAlarmEventToDownlink(EdgeEvent edgeEvent) { - AlarmUpdateMsg alarmUpdateMsg = - convertAlarmEventToAlarmMsg(edgeEvent.getTenantId(), edgeEvent.getEntityId(), edgeEvent.getAction(), edgeEvent.getBody()); + public DownlinkMsg convertAlarmEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + AlarmUpdateMsg alarmUpdateMsg = convertAlarmEventToAlarmMsg(edgeEvent.getTenantId(), edgeEvent.getEntityId(), + edgeEvent.getAction(), edgeEvent.getBody(), edgeVersion); if (alarmUpdateMsg != null) { return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) @@ -122,5 +123,4 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor { } while (pageData != null && pageData.hasNext()); return futures; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index 3fb086cd52..99b8d2c12a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -37,18 +37,29 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @Slf4j public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { - protected ListenableFuture processAlarmMsg(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { - EntityId originatorId = getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), - EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); + public ListenableFuture processAlarmMsg(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg, EdgeVersion edgeVersion) { + log.trace("[{}] processAlarmMsg [{}]", tenantId, alarmUpdateMsg); AlarmId alarmId = new AlarmId(new UUID(alarmUpdateMsg.getIdMSB(), alarmUpdateMsg.getIdLSB())); + boolean isEdgeVersionOlderThan_3_6_2 = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion); + Alarm alarm = isEdgeVersionOlderThan_3_6_2 ? createDeprecatedAlarm(tenantId, alarmUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(alarmUpdateMsg.getEntity(), Alarm.class); + if (alarm == null) { + throw new RuntimeException("[{" + tenantId + "}] alarmUpdateMsg {" + alarmUpdateMsg + "} cannot be converted to alarm"); + } + EntityId originatorId = isEdgeVersionOlderThan_3_6_2 + ? getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), EntityType.valueOf(alarmUpdateMsg.getOriginatorType())) + : alarm.getOriginator(); if (originatorId == null) { log.warn("[{}] Originator not found for the alarm msg {}", tenantId, alarmUpdateMsg); return Futures.immediateFuture(null); @@ -58,21 +69,8 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { switch (alarmUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - Alarm alarm = new Alarm(); alarm.setId(alarmId); - alarm.setTenantId(tenantId); - alarm.setType(alarmUpdateMsg.getName()); alarm.setOriginator(originatorId); - alarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); - alarm.setStartTs(alarmUpdateMsg.getStartTs()); - var alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus()); - alarm.setClearTs(alarmUpdateMsg.getClearTs()); - alarm.setPropagate(alarmUpdateMsg.getPropagate()); - alarm.setCleared(alarmStatus.isCleared()); - alarm.setAcknowledged(alarmStatus.isAck()); - alarm.setAckTs(alarmUpdateMsg.getAckTs()); - alarm.setEndTs(alarmUpdateMsg.getEndTs()); - alarm.setDetails(JacksonUtil.toJsonNode(alarmUpdateMsg.getDetails())); if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(alarmUpdateMsg.getMsgType())) { alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, null, alarmId)); } else { @@ -82,14 +80,13 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_ACK_RPC_MESSAGE: Alarm alarmToAck = alarmService.findAlarmById(tenantId, alarmId); if (alarmToAck != null) { - alarmService.acknowledgeAlarm(tenantId, alarmId, alarmUpdateMsg.getAckTs()); + alarmService.acknowledgeAlarm(tenantId, alarmId, alarm.getAckTs()); } break; case ALARM_CLEAR_RPC_MESSAGE: Alarm alarmToClear = alarmService.findAlarmById(tenantId, alarmId); if (alarmToClear != null) { - alarmService.clearAlarm(tenantId, alarmId, alarmUpdateMsg.getClearTs(), - JacksonUtil.toJsonNode(alarmUpdateMsg.getDetails())); + alarmService.clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails()); } break; case ENTITY_DELETED_RPC_MESSAGE: @@ -109,7 +106,24 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } - protected AlarmUpdateMsg convertAlarmEventToAlarmMsg(TenantId tenantId, UUID entityId, EdgeEventActionType actionType, JsonNode body) { + private Alarm createDeprecatedAlarm(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { + Alarm alarm = new Alarm(); + alarm.setTenantId(tenantId); + alarm.setType(alarmUpdateMsg.getName()); + alarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); + alarm.setStartTs(alarmUpdateMsg.getStartTs()); + AlarmStatus alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus()); + alarm.setClearTs(alarmUpdateMsg.getClearTs()); + alarm.setPropagate(alarmUpdateMsg.getPropagate()); + alarm.setCleared(alarmStatus.isCleared()); + alarm.setAcknowledged(alarmStatus.isAck()); + alarm.setAckTs(alarmUpdateMsg.getAckTs()); + alarm.setEndTs(alarmUpdateMsg.getEndTs()); + alarm.setDetails(JacksonUtil.toJsonNode(alarmUpdateMsg.getDetails())); + return alarm; + } + + public AlarmUpdateMsg convertAlarmEventToAlarmMsg(TenantId tenantId, UUID entityId, EdgeEventActionType actionType, JsonNode body, EdgeVersion edgeVersion) { AlarmId alarmId = new AlarmId(entityId); UpdateMsgType msgType = getUpdateMsgType(actionType); switch (actionType) { @@ -119,12 +133,16 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_CLEAR: Alarm alarm = alarmService.findAlarmById(tenantId, alarmId); if (alarm != null) { - return alarmMsgConstructor.constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); + return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); } break; case DELETED: Alarm deletedAlarm = JacksonUtil.convertValue(body, Alarm.class); - return alarmMsgConstructor.constructAlarmUpdatedMsg(msgType, deletedAlarm, findOriginatorEntityName(tenantId, deletedAlarm)); + if (deletedAlarm != null) { + return ((AlarmMsgConstructor) alarmMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructAlarmUpdatedMsg(msgType, deletedAlarm, findOriginatorEntityName(tenantId, deletedAlarm)); + } } return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index caa03af8a5..e760a4d73a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -41,6 +41,8 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @@ -49,7 +51,7 @@ import java.util.UUID; @TbCoreComponent public class AssetEdgeProcessor extends BaseAssetProcessor { - public ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg) { + public ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processAssetMsgFromEdge [{}] from edge [{}]", tenantId, assetUpdateMsg, edge.getId()); AssetId assetId = new AssetId(new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB())); try { @@ -58,7 +60,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { switch (assetUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge); + saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge, edgeVersion); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: Asset assetToDelete = assetService.findAssetById(tenantId, assetId); @@ -82,9 +84,8 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { } } - private void saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, Edge edge) { - CustomerId customerId = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); - Pair resultPair = super.saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, customerId); + private void saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + Pair resultPair = super.saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edgeVersion); Boolean created = resultPair.getFirst(); if (created) { createRelationFromEdge(tenantId, edge.getId(), assetId); @@ -120,23 +121,24 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); if (asset != null && !BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - AssetUpdateMsg assetUpdateMsg = - assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset); + AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) + assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetUpdatedMsg(msgType, asset); DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion); - builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); + builder.addAssetProfileUpdateMsg(((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructAssetProfileUpdatedMsg(msgType, assetProfile)); } downlinkMsg = builder.build(); } break; case DELETED: case UNASSIGNED_FROM_EDGE: - AssetUpdateMsg assetUpdateMsg = - assetMsgConstructor.constructAssetDeleteMsg(assetId); + AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor) + assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetDeleteMsg(assetId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg) @@ -145,4 +147,12 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { } return downlinkMsg; } + + @Override + protected void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg, EdgeVersion edgeVersion) { + CustomerId customerUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()) + : asset.getCustomerId() != null ? asset.getCustomerId() : customerId; + asset.setCustomerId(customerUUID); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java index fb55edd8c5..a18030992d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java @@ -40,6 +40,8 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @@ -48,7 +50,7 @@ import java.util.UUID; @TbCoreComponent public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { - public ListenableFuture processAssetProfileMsgFromEdge(TenantId tenantId, Edge edge, AssetProfileUpdateMsg assetProfileUpdateMsg) { + public ListenableFuture processAssetProfileMsgFromEdge(TenantId tenantId, Edge edge, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processAssetProfileMsgFromEdge [{}] from edge [{}]", tenantId, assetProfileUpdateMsg, edge.getId()); AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB())); try { @@ -57,7 +59,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { switch (assetProfileUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edge); + saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edge, edgeVersion); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: case UNRECOGNIZED: @@ -72,8 +74,8 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { } } - private void saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, Edge edge) { - Pair resultPair = super.saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg); + private void saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + Pair resultPair = super.saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edgeVersion); Boolean created = resultPair.getFirst(); if (created) { createRelationFromEdge(tenantId, edge.getId(), assetProfileId); @@ -106,8 +108,8 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { if (assetProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion); - AssetProfileUpdateMsg assetProfileUpdateMsg = - assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); + AssetProfileUpdateMsg assetProfileUpdateMsg = ((AssetMsgConstructor) + assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetProfileUpdatedMsg(msgType, assetProfile); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetProfileUpdateMsg(assetProfileUpdateMsg) @@ -115,8 +117,8 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { } break; case DELETED: - AssetProfileUpdateMsg assetProfileUpdateMsg = - assetProfileMsgConstructor.constructAssetProfileDeleteMsg(assetProfileId); + AssetProfileUpdateMsg assetProfileUpdateMsg = ((AssetMsgConstructor) + assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetProfileDeleteMsg(assetProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetProfileUpdateMsg(assetProfileUpdateMsg) @@ -127,19 +129,23 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { } @Override - protected void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { - // do nothing on cloud + protected void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, RuleChainId ruleChainId) { + assetProfile.setDefaultRuleChainId(ruleChainId); } @Override - protected void setDefaultEdgeRuleChainId(TenantId tenantId,AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { - UUID defaultEdgeRuleChainUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); + protected void setDefaultEdgeRuleChainId(AssetProfile assetProfile, RuleChainId ruleChainId, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion) { + UUID defaultEdgeRuleChainUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()) + : ruleChainId != null ? ruleChainId.getId() : null; assetProfile.setDefaultEdgeRuleChainId(defaultEdgeRuleChainUUID != null ? new RuleChainId(defaultEdgeRuleChainUUID) : null); } @Override - protected void setDefaultDashboardId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg) { - UUID defaultDashboardUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB()); + protected void setDefaultDashboardId(TenantId tenantId, DashboardId dashboardId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion) { + UUID defaultDashboardUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB()) + : assetProfile.getDefaultDashboardId() != null ? assetProfile.getDefaultDashboardId().getId() : (dashboardId != null ? dashboardId.getId() : null); assetProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java index f8dcdbd8f6..6403631f50 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -26,43 +26,43 @@ import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @Slf4j public abstract class BaseAssetProcessor extends BaseEdgeProcessor { - protected Pair saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, CustomerId customerId) { + protected Pair saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, EdgeVersion edgeVersion) { boolean created = false; boolean assetNameUpdated = false; assetCreationLock.lock(); try { - Asset asset = assetService.findAssetById(tenantId, assetId); - String assetName = assetUpdateMsg.getName(); + Asset asset = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createAsset(tenantId, assetId, assetUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); if (asset == null) { + throw new RuntimeException("[{" + tenantId + "}] assetUpdateMsg {" + assetUpdateMsg + " } cannot be converted to asset"); + } + Asset assetById = assetService.findAssetById(tenantId, assetId); + if (assetById == null) { created = true; - asset = new Asset(); - asset.setTenantId(tenantId); - asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId())); + asset.setId(null); + } else { + asset.setId(assetId); } + String assetName = asset.getName(); Asset assetByName = assetService.findAssetByTenantIdAndName(tenantId, assetName); if (assetByName != null && !assetByName.getId().equals(assetId)) { assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); log.warn("[{}] Asset with name {} already exists. Renaming asset name to {}", - tenantId, assetUpdateMsg.getName(), assetName); + tenantId, asset.getName(), assetName); assetNameUpdated = true; } asset.setName(assetName); - asset.setType(assetUpdateMsg.getType()); - asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null); - asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo() - ? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null); - - UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB()); - asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null); - - asset.setCustomerId(customerId); + setCustomerId(tenantId, created ? null : assetById.getCustomerId(), asset, assetUpdateMsg, edgeVersion); assetValidator.validate(asset, Asset::getTenantId); if (created) { @@ -77,4 +77,24 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { } return Pair.of(created, assetNameUpdated); } + + private Asset createAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg) { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName(assetUpdateMsg.getName()); + asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId())); + asset.setType(assetUpdateMsg.getType()); + asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null); + asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo() + ? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null); + + UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB()); + asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null); + + CustomerId customerId = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); + asset.setCustomerId(customerId); + return asset; + } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg, EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java index 67c8f8de44..68d52e74b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java @@ -18,54 +18,66 @@ package org.thingsboard.server.service.edge.rpc.processor.asset; import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.nio.charset.StandardCharsets; @Slf4j public abstract class BaseAssetProfileProcessor extends BaseEdgeProcessor { - protected Pair saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) { + protected Pair saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion) { boolean created = false; boolean assetProfileNameUpdated = false; assetCreationLock.lock(); try { - AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); - String assetProfileName = assetProfileUpdateMsg.getName(); + AssetProfile assetProfile = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(assetProfileUpdateMsg.getEntity(), AssetProfile.class); if (assetProfile == null) { + throw new RuntimeException("[{" + tenantId + "}] assetProfileUpdateMsg {" + assetProfileUpdateMsg + "} cannot be converted to asset profile"); + } + boolean isDefault = assetProfile.isDefault(); + AssetProfile assetProfileById = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + if (assetProfileById == null) { created = true; - assetProfile = new AssetProfile(); - assetProfile.setTenantId(tenantId); - assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId())); + assetProfile.setId(null); + } else { + assetProfile.setId(assetProfileId); } + assetProfile.setDefault(false); + String assetProfileName = assetProfile.getName(); AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfileName); if (assetProfileByName != null && !assetProfileByName.getId().equals(assetProfileId)) { assetProfileName = assetProfileName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] Asset profile with name {} already exists. Renaming asset profile name to {}", - tenantId, assetProfileUpdateMsg.getName(), assetProfileName); + tenantId, assetProfile.getName(), assetProfileName); assetProfileNameUpdated = true; } assetProfile.setName(assetProfileName); - assetProfile.setDefault(assetProfileUpdateMsg.getDefault()); - assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null); - assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null); - assetProfile.setImage(assetProfileUpdateMsg.hasImage() - ? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); - setDefaultRuleChainId(tenantId, assetProfile, assetProfileUpdateMsg); - setDefaultEdgeRuleChainId(tenantId, assetProfile, assetProfileUpdateMsg); - setDefaultDashboardId(tenantId, assetProfile, assetProfileUpdateMsg); + RuleChainId ruleChainId = assetProfile.getDefaultRuleChainId(); + setDefaultRuleChainId(tenantId, assetProfile, created ? null : assetProfileById.getDefaultRuleChainId()); + setDefaultEdgeRuleChainId(assetProfile, ruleChainId, assetProfileUpdateMsg, edgeVersion); + setDefaultDashboardId(tenantId, created ? null : assetProfileById.getDefaultDashboardId(), assetProfile, assetProfileUpdateMsg, edgeVersion); assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId); if (created) { assetProfile.setId(assetProfileId); } assetProfileService.saveAssetProfile(assetProfile, false); + if (isDefault) { + assetProfileService.setDefaultAssetProfile(tenantId, assetProfileId); + } } catch (Exception e) { log.error("[{}] Failed to process asset profile update msg [{}]", tenantId, assetProfileUpdateMsg, e); throw e; @@ -75,9 +87,22 @@ public abstract class BaseAssetProfileProcessor extends BaseEdgeProcessor { return Pair.of(created, assetProfileNameUpdated); } - protected abstract void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); + private AssetProfile createAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfile.setName(assetProfileUpdateMsg.getName()); + assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId())); + assetProfile.setDefault(assetProfileUpdateMsg.getDefault()); + assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null); + assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null); + assetProfile.setImage(assetProfileUpdateMsg.hasImage() + ? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + return assetProfile; + } + + protected abstract void setDefaultRuleChainId(TenantId tenantId, AssetProfile assetProfile, RuleChainId ruleChainId); - protected abstract void setDefaultEdgeRuleChainId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); + protected abstract void setDefaultEdgeRuleChainId(AssetProfile assetProfile, RuleChainId ruleChainId, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion); - protected abstract void setDefaultDashboardId(TenantId tenantId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg); + protected abstract void setDefaultDashboardId(TenantId tenantId, DashboardId dashboardId, AssetProfile assetProfile, AssetProfileUpdateMsg assetProfileUpdateMsg, EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index f553d6d5a0..9b9d99fcc3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -33,9 +33,11 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import java.util.ArrayList; @@ -47,7 +49,7 @@ import java.util.UUID; @TbCoreComponent public class CustomerEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertCustomerEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertCustomerEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -56,8 +58,8 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { Customer customer = customerService.findCustomerById(edgeEvent.getTenantId(), customerId); if (customer != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - CustomerUpdateMsg customerUpdateMsg = - customerMsgConstructor.constructCustomerUpdatedMsg(msgType, customer); + CustomerUpdateMsg customerUpdateMsg = ((CustomerMsgConstructor) + customerMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructCustomerUpdatedMsg(msgType, customer); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addCustomerUpdateMsg(customerUpdateMsg) @@ -65,8 +67,8 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor { } break; case DELETED: - CustomerUpdateMsg customerUpdateMsg = - customerMsgConstructor.constructCustomerDeleteMsg(customerId); + CustomerUpdateMsg customerUpdateMsg = ((CustomerMsgConstructor) + customerMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructCustomerDeleteMsg(customerId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addCustomerUpdateMsg(customerUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java index 687e14a07d..0ae26d5160 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -25,60 +25,88 @@ 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.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.Set; @Slf4j public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { - protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId) { + protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId, EdgeVersion edgeVersion) { boolean created = false; - Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); + Dashboard dashboard = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createDashboard(tenantId, dashboardId, dashboardUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); if (dashboard == null) { - created = true; - dashboard = new Dashboard(); - dashboard.setTenantId(tenantId); - dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId())); + throw new RuntimeException("[{" + tenantId + "}] dashboardUpdateMsg {" + dashboardUpdateMsg + "} cannot be converted to dashboard"); } - dashboard.setTitle(dashboardUpdateMsg.getTitle()); - dashboard.setImage(dashboardUpdateMsg.hasImage() ? dashboardUpdateMsg.getImage() : null); - dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration())); - Set assignedCustomers = null; - if (dashboardUpdateMsg.hasAssignedCustomers()) { - assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {}); - assignedCustomers = filterNonExistingCustomers(tenantId, assignedCustomers); - dashboard.setAssignedCustomers(assignedCustomers); + Dashboard dashboardById = dashboardService.findDashboardById(tenantId, dashboardId); + if (dashboardById == null) { + created = true; + dashboard.setId(null); + } else { + dashboard.setId(dashboardId); + assignedCustomers = filterNonExistingCustomers(tenantId, dashboardById.getAssignedCustomers()); } - dashboard.setMobileOrder(dashboardUpdateMsg.hasMobileOrder() ? dashboardUpdateMsg.getMobileOrder() : null); - dashboard.setMobileHide(dashboardUpdateMsg.getMobileHide()); - dashboardValidator.validate(dashboard, Dashboard::getTenantId); if (created) { dashboard.setId(dashboardId); } + Set msgAssignedCustomers = filterNonExistingCustomers(tenantId, dashboard.getAssignedCustomers()); + if (msgAssignedCustomers != null) { + if (assignedCustomers == null) { + assignedCustomers = msgAssignedCustomers; + } else { + assignedCustomers.addAll(msgAssignedCustomers); + } + } + dashboard.setAssignedCustomers(assignedCustomers); Dashboard savedDashboard = dashboardService.saveDashboard(dashboard, false); - if (assignedCustomers != null && !assignedCustomers.isEmpty()) { - for (ShortCustomerInfo assignedCustomer : assignedCustomers) { + if (msgAssignedCustomers != null && !msgAssignedCustomers.isEmpty()) { + for (ShortCustomerInfo assignedCustomer : msgAssignedCustomers) { if (assignedCustomer.getCustomerId().equals(customerId)) { - dashboardService.assignDashboardToCustomer(tenantId, dashboardId, assignedCustomer.getCustomerId()); + dashboardService.assignDashboardToCustomer(tenantId, savedDashboard.getId(), assignedCustomer.getCustomerId()); } } } else { - unassignCustomersFromDashboard(tenantId, savedDashboard); + unassignCustomersFromDashboard(tenantId, savedDashboard, customerId); } return created; } - protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers); + private Dashboard createDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg) { + Dashboard dashboard = new Dashboard(); + dashboard.setTenantId(tenantId); + dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId())); + dashboard.setTitle(dashboardUpdateMsg.getTitle()); + dashboard.setImage(dashboardUpdateMsg.hasImage() ? dashboardUpdateMsg.getImage() : null); + dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration())); + + Set assignedCustomers; + if (dashboardUpdateMsg.hasAssignedCustomers()) { + assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {}); + assignedCustomers = filterNonExistingCustomers(tenantId, assignedCustomers); + dashboard.setAssignedCustomers(assignedCustomers); + } - private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard) { + dashboard.setMobileOrder(dashboardUpdateMsg.hasMobileOrder() ? dashboardUpdateMsg.getMobileOrder() : null); + dashboard.setMobileHide(dashboardUpdateMsg.getMobileHide()); + return dashboard; + } + + private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard, CustomerId customerId) { if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) { for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) { - dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId()); + if (assignedCustomer.getCustomerId().equals(customerId)) { + dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId()); + } } } } + + protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index 291491a2fe..3104e1ac35 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; -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.msg.TbMsgType; @@ -33,8 +32,10 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructor; import java.util.Set; import java.util.UUID; @@ -44,7 +45,7 @@ import java.util.UUID; @TbCoreComponent public class DashboardEdgeProcessor extends BaseDashboardProcessor { - public ListenableFuture processDashboardMsgFromEdge(TenantId tenantId, Edge edge, DashboardUpdateMsg dashboardUpdateMsg) { + public ListenableFuture processDashboardMsgFromEdge(TenantId tenantId, Edge edge, DashboardUpdateMsg dashboardUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processDashboardMsgFromEdge [{}] from edge [{}]", tenantId, dashboardUpdateMsg, edge.getId()); DashboardId dashboardId = new DashboardId(new UUID(dashboardUpdateMsg.getIdMSB(), dashboardUpdateMsg.getIdLSB())); try { @@ -53,7 +54,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor { switch (dashboardUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge); + saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge, edgeVersion); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: Dashboard dashboardToDelete = dashboardService.findDashboardById(tenantId, dashboardId); @@ -77,9 +78,9 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor { } } - private void saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, Edge edge) { - CustomerId customerId = safeGetCustomerId(dashboardUpdateMsg.getCustomerIdMSB(), dashboardUpdateMsg.getCustomerIdLSB()); - boolean created = super.saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, customerId); + private void saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + boolean created = super.saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, + edge.getCustomerId(), edgeVersion); if (created) { createRelationFromEdge(tenantId, edge.getId(), dashboardId); pushDashboardCreatedEventToRuleEngine(tenantId, edge, dashboardId); @@ -98,7 +99,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor { } } - public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -110,8 +111,8 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor { Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId); if (dashboard != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DashboardUpdateMsg dashboardUpdateMsg = - dashboardMsgConstructor.constructDashboardUpdatedMsg(msgType, dashboard); + DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) + dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardUpdatedMsg(msgType, dashboard); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDashboardUpdateMsg(dashboardUpdateMsg) @@ -120,8 +121,8 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor { break; case DELETED: case UNASSIGNED_FROM_EDGE: - DashboardUpdateMsg dashboardUpdateMsg = - dashboardMsgConstructor.constructDashboardDeleteMsg(dashboardId); + DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor) + dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardDeleteMsg(dashboardId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDashboardUpdateMsg(dashboardUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 8958db0c44..e2ac06e84b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -32,8 +32,10 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.Optional; import java.util.UUID; @@ -44,46 +46,35 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { @Autowired protected DataDecodingEncodingService dataDecodingEncodingService; - protected Pair saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, CustomerId customerId) { + protected Pair saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, EdgeVersion edgeVersion) { boolean created = false; boolean deviceNameUpdated = false; deviceCreationLock.lock(); try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - String deviceName = deviceUpdateMsg.getName(); + Device device = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createDevice(tenantId, deviceId, deviceUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); if (device == null) { + throw new RuntimeException("[{" + tenantId + "}] deviceUpdateMsg {" + deviceUpdateMsg + "} cannot be converted to device"); + } + Device deviceById = deviceService.findDeviceById(tenantId, deviceId); + if (deviceById == null) { created = true; - device = new Device(); - device.setTenantId(tenantId); - device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); + device.setId(null); + } else { + device.setId(deviceId); } + String deviceName = device.getName(); Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); if (deviceByName != null && !deviceByName.getId().equals(deviceId)) { deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] Device with name {} already exists. Renaming device name to {}", - tenantId, deviceUpdateMsg.getName(), deviceName); + tenantId, device.getName(), deviceName); deviceNameUpdated = true; } device.setName(deviceName); - device.setType(deviceUpdateMsg.getType()); - device.setLabel(deviceUpdateMsg.hasLabel() ? deviceUpdateMsg.getLabel() : null); - device.setAdditionalInfo(deviceUpdateMsg.hasAdditionalInfo() - ? JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo()) : null); - - UUID deviceProfileUUID = safeGetUUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB()); - device.setDeviceProfileId(deviceProfileUUID != null ? new DeviceProfileId(deviceProfileUUID) : null); - - device.setCustomerId(customerId); + setCustomerId(tenantId, created ? null : deviceById.getCustomerId(), device, deviceUpdateMsg, edgeVersion); - Optional deviceDataOpt = - dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); - device.setDeviceData(deviceDataOpt.orElse(null)); - - UUID firmwareUUID = safeGetUUID(deviceUpdateMsg.getFirmwareIdMSB(), deviceUpdateMsg.getFirmwareIdLSB()); - device.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); - - UUID softwareUUID = safeGetUUID(deviceUpdateMsg.getSoftwareIdMSB(), deviceUpdateMsg.getSoftwareIdLSB()); - device.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); deviceValidator.validate(device, Device::getTenantId); if (created) { device.setId(deviceId); @@ -100,25 +91,53 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { } catch (Exception e) { log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e); throw e; - } finally { + } finally { deviceCreationLock.unlock(); } return Pair.of(created, deviceNameUpdated); } - protected void updateDeviceCredentials(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { - DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); - Device device = deviceService.findDeviceById(tenantId, deviceId); + private Device createDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); + device.setName(deviceUpdateMsg.getName()); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.hasLabel() ? deviceUpdateMsg.getLabel() : null); + device.setAdditionalInfo(deviceUpdateMsg.hasAdditionalInfo() + ? JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo()) : null); + + UUID deviceProfileUUID = safeGetUUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB()); + device.setDeviceProfileId(deviceProfileUUID != null ? new DeviceProfileId(deviceProfileUUID) : null); + + Optional deviceDataOpt = dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); + device.setDeviceData(deviceDataOpt.orElse(null)); + + UUID firmwareUUID = safeGetUUID(deviceUpdateMsg.getFirmwareIdMSB(), deviceUpdateMsg.getFirmwareIdLSB()); + device.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); + UUID softwareUUID = safeGetUUID(deviceUpdateMsg.getSoftwareIdMSB(), deviceUpdateMsg.getSoftwareIdLSB()); + device.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + + return device; + } + + protected void updateDeviceCredentials(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg, EdgeVersion edgeVersion) { + DeviceCredentials deviceCredentials = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createDeviceCredentials(deviceCredentialsUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class); + if (deviceCredentials == null) { + throw new RuntimeException("[{" + tenantId + "}] deviceCredentialsUpdateMsg {" + deviceCredentialsUpdateMsg + "} cannot be converted to device credentials"); + } + Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId()); if (device != null) { log.debug("[{}] Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", - tenantId, device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); + tenantId, device.getName(), deviceCredentials.getCredentialsId(), deviceCredentials.getCredentialsValue()); try { - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); - deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); - deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); - deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.hasCredentialsValue() - ? deviceCredentialsUpdateMsg.getCredentialsValue() : null); - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + DeviceCredentials deviceCredentialsByDeviceId = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); + deviceCredentialsByDeviceId.setCredentialsType(deviceCredentials.getCredentialsType()); + deviceCredentialsByDeviceId.setCredentialsId(deviceCredentials.getCredentialsId()); + deviceCredentialsByDeviceId.setCredentialsValue(deviceCredentials.getCredentialsValue()); + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentialsByDeviceId); } catch (Exception e) { log.error("[{}] Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", @@ -126,7 +145,19 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { throw new RuntimeException(e); } } else { - log.warn("[{}] Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", tenantId, deviceId, deviceCredentialsUpdateMsg); + log.warn("[{}] Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentials.getDeviceId(), deviceCredentialsUpdateMsg); } } + + private DeviceCredentials createDeviceCredentials(DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB()))); + deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); + deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); + deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.hasCredentialsValue() + ? deviceCredentialsUpdateMsg.getCredentialsValue() : null); + return deviceCredentials; + } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg, EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java index bd35a42e59..55153a7a79 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java @@ -19,18 +19,23 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; 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.StringUtils; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -42,74 +47,95 @@ public abstract class BaseDeviceProfileProcessor extends BaseEdgeProcessor { @Autowired private DataDecodingEncodingService dataDecodingEncodingService; - protected Pair saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + protected Pair saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion) { boolean created = false; boolean deviceProfileNameUpdated = false; deviceCreationLock.lock(); try { - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); - String deviceProfileName = deviceProfileUpdateMsg.getName(); + DeviceProfile deviceProfile = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); if (deviceProfile == null) { + throw new RuntimeException("[{" + tenantId + "}] deviceProfileUpdateMsg {" + deviceProfileUpdateMsg + "} cannot be converted to device profile"); + } + boolean isDefault = deviceProfile.isDefault(); + DeviceProfile deviceProfileById = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + if (deviceProfileById == null) { created = true; - deviceProfile = new DeviceProfile(); - deviceProfile.setTenantId(tenantId); - deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId())); + deviceProfile.setId(null); + } else { + deviceProfile.setId(deviceProfileId); } + String deviceProfileName = deviceProfile.getName(); DeviceProfile deviceProfileByName = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfileName); if (deviceProfileByName != null && !deviceProfileByName.getId().equals(deviceProfileId)) { deviceProfileName = deviceProfileName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] Device profile with name {} already exists. Renaming device profile name to {}", - tenantId, deviceProfileUpdateMsg.getName(), deviceProfileName); + tenantId, deviceProfile.getName(), deviceProfileName); deviceProfileNameUpdated = true; } + deviceProfile.setDefault(false); deviceProfile.setName(deviceProfileName); - deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null); - deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType())); - deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType() - ? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT); - deviceProfile.setImage(deviceProfileUpdateMsg.hasImage() - ? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); - deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType() - ? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED); - deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey() - ? deviceProfileUpdateMsg.getProvisionDeviceKey() : null); - deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName()); - - Optional profileDataOpt = - dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); - deviceProfile.setProfileData(profileDataOpt.orElse(null)); - - setDefaultRuleChainId(tenantId, deviceProfile, deviceProfileUpdateMsg); - setDefaultEdgeRuleChainId(tenantId, deviceProfile, deviceProfileUpdateMsg); - setDefaultDashboardId(tenantId, deviceProfile, deviceProfileUpdateMsg); - String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName()) - ? deviceProfileUpdateMsg.getDefaultQueueName() : null; - deviceProfile.setDefaultQueueName(defaultQueueName); - - UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB()); - deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); - - UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB()); - deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + RuleChainId ruleChainId = deviceProfile.getDefaultRuleChainId(); + setDefaultRuleChainId(tenantId, deviceProfile, created ? null : deviceProfileById.getDefaultRuleChainId()); + setDefaultEdgeRuleChainId(deviceProfile, ruleChainId, deviceProfileUpdateMsg, edgeVersion); + setDefaultDashboardId(tenantId, created ? null : deviceProfileById.getDefaultDashboardId(), deviceProfile, deviceProfileUpdateMsg, edgeVersion); deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); if (created) { deviceProfile.setId(deviceProfileId); } deviceProfileService.saveDeviceProfile(deviceProfile, false); + if (isDefault) { + deviceProfileService.setDefaultDeviceProfile(tenantId, deviceProfileId); + } } catch (Exception e) { log.error("[{}] Failed to process device profile update msg [{}]", tenantId, deviceProfileUpdateMsg, e); throw e; - } finally { + } finally { deviceCreationLock.unlock(); } return Pair.of(created, deviceProfileNameUpdated); } - protected abstract void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); + private DeviceProfile createDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId())); + deviceProfile.setName(deviceProfileUpdateMsg.getName()); + deviceProfile.setDefault(deviceProfileUpdateMsg.getDefault()); + deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null); + deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType())); + deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType() + ? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT); + deviceProfile.setImage(deviceProfileUpdateMsg.hasImage() + ? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType() + ? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED); + deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey() + ? deviceProfileUpdateMsg.getProvisionDeviceKey() : null); + deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName()); + + Optional profileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + deviceProfile.setProfileData(profileDataOpt.orElse(null)); + + String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName()) + ? deviceProfileUpdateMsg.getDefaultQueueName() : null; + deviceProfile.setDefaultQueueName(defaultQueueName); + + UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB()); + deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); + + UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB()); + deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + return deviceProfile; + } + + protected abstract void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, RuleChainId ruleChainId); - protected abstract void setDefaultEdgeRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); + protected abstract void setDefaultEdgeRuleChainId(DeviceProfile deviceProfile, RuleChainId ruleChainId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion); - protected abstract void setDefaultDashboardId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg); + protected abstract void setDefaultDashboardId(TenantId tenantId, DashboardId dashboardId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 910be10240..b65ef58e1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -54,7 +55,8 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @@ -63,7 +65,7 @@ import java.util.UUID; @TbCoreComponent public class DeviceEdgeProcessor extends BaseDeviceProcessor { - public ListenableFuture processDeviceMsgFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + public ListenableFuture processDeviceMsgFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processDeviceMsgFromEdge [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getId()); DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); try { @@ -72,7 +74,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { switch (deviceUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); + saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge, edgeVersion); return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); case ENTITY_DELETED_RPC_MESSAGE: Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); @@ -96,21 +98,20 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } } - public ListenableFuture processDeviceCredentialsMsgFromEdge(TenantId tenantId, EdgeId edgeId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + public ListenableFuture processDeviceCredentialsMsgFromEdge(TenantId tenantId, EdgeId edgeId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg, EdgeVersion edgeVersion) { log.debug("[{}] Executing processDeviceCredentialsMsgFromEdge, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg); try { edgeSynchronizationManager.getEdgeId().set(edgeId); - updateDeviceCredentials(tenantId, deviceCredentialsUpdateMsg); + updateDeviceCredentials(tenantId, deviceCredentialsUpdateMsg, edgeVersion); } finally { edgeSynchronizationManager.getEdgeId().remove(); } return Futures.immediateFuture(null); } - private void saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, Edge edge) { - CustomerId customerId = safeGetCustomerId(deviceUpdateMsg.getCustomerIdMSB(), deviceUpdateMsg.getCustomerIdLSB()); - Pair resultPair = super.saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, customerId); + private void saveOrUpdateDevice(TenantId tenantId, DeviceId deviceId, DeviceUpdateMsg deviceUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + Pair resultPair = super.saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edgeVersion); Boolean created = resultPair.getFirst(); if (created) { createRelationFromEdge(tenantId, edge.getId(), deviceId); @@ -227,23 +228,26 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId); if (device != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceUpdateMsg deviceUpdateMsg = - deviceMsgConstructor.constructDeviceUpdatedMsg(msgType, device); + DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructDeviceUpdatedMsg(msgType, device); DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg); if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + builder.addDeviceProfileUpdateMsg(((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); } downlinkMsg = builder.build(); } break; case DELETED: case UNASSIGNED_FROM_EDGE: - DeviceUpdateMsg deviceUpdateMsg = - deviceMsgConstructor.constructDeviceDeleteMsg(deviceId); + DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceDeleteMsg(deviceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg) @@ -252,8 +256,8 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { case CREDENTIALS_UPDATED: DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = - deviceMsgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) @@ -261,20 +265,18 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } break; case RPC_CALL: - return convertRpcCallEventToDownlink(edgeEvent); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addDeviceRpcCallMsg(((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) + .build(); case CREDENTIALS_REQUEST: return convertCredentialsRequestEventToDownlink(edgeEvent); } return downlinkMsg; } - private DownlinkMsg convertRpcCallEventToDownlink(EdgeEvent edgeEvent) { - return DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(deviceMsgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) - .build(); - } - private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() @@ -286,4 +288,12 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); return builder.build(); } + + @Override + protected void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg, EdgeVersion edgeVersion) { + CustomerId customerUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetCustomerId(deviceUpdateMsg.getCustomerIdMSB(), deviceUpdateMsg.getCustomerIdLSB()) + : device.getCustomerId() != null ? device.getCustomerId() : customerId; + device.setCustomerId(customerUUID); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java index 3ed75bdb41..c4105f9668 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java @@ -40,6 +40,8 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @@ -48,7 +50,7 @@ import java.util.UUID; @TbCoreComponent public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { - public ListenableFuture processDeviceProfileMsgFromEdge(TenantId tenantId, Edge edge, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + public ListenableFuture processDeviceProfileMsgFromEdge(TenantId tenantId, Edge edge, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processDeviceProfileMsgFromEdge [{}] from edge [{}]", tenantId, deviceProfileUpdateMsg, edge.getId()); DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB())); try { @@ -57,7 +59,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { switch (deviceProfileUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edge); + saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edge, edgeVersion); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: case UNRECOGNIZED: @@ -72,8 +74,8 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { } } - private void saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, Edge edge) { - Pair resultPair = super.saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg); + private void saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + Pair resultPair = super.saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edgeVersion); Boolean created = resultPair.getFirst(); if (created) { createRelationFromEdge(tenantId, edge.getId(), deviceProfileId); @@ -106,8 +108,8 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = - deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileUpdatedMsg(msgType, deviceProfile); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -115,8 +117,8 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { } break; case DELETED: - DeviceProfileUpdateMsg deviceProfileUpdateMsg = - deviceProfileMsgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -127,19 +129,23 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { } @Override - protected void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { - // do nothing on cloud + protected void setDefaultRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, RuleChainId ruleChainId) { + deviceProfile.setDefaultRuleChainId(ruleChainId); } @Override - protected void setDefaultEdgeRuleChainId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { - UUID defaultEdgeRuleChainUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); + protected void setDefaultEdgeRuleChainId(DeviceProfile deviceProfile, RuleChainId ruleChainId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion) { + UUID defaultEdgeRuleChainUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()) + : ruleChainId != null ? ruleChainId.getId() : null; deviceProfile.setDefaultEdgeRuleChainId(defaultEdgeRuleChainUUID != null ? new RuleChainId(defaultEdgeRuleChainUUID) : null); } @Override - protected void setDefaultDashboardId(TenantId tenantId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { - UUID defaultDashboardUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); + protected void setDefaultDashboardId(TenantId tenantId, DashboardId dashboardId, DeviceProfile deviceProfile, DeviceProfileUpdateMsg deviceProfileUpdateMsg, EdgeVersion edgeVersion) { + UUID defaultDashboardUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()) + : deviceProfile.getDefaultDashboardId() != null ? deviceProfile.getDefaultDashboardId().getId() : (dashboardId != null ? dashboardId.getId() : null); deviceProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java index cca0057707..723f3e0e2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -27,50 +27,72 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.EdgeEntityType; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @Slf4j public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { - protected Pair saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, CustomerId customerId) { + protected Pair saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, EdgeVersion edgeVersion) { boolean created = false; boolean entityViewNameUpdated = false; - EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); - String entityViewName = entityViewUpdateMsg.getName(); + EntityView entityView = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createEntityView(tenantId, entityViewId, entityViewUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(entityViewUpdateMsg.getEntity(), EntityView.class); if (entityView == null) { + throw new RuntimeException("[{" + tenantId + "}] entityViewUpdateMsg {" + entityViewUpdateMsg + "} cannot be converted to entity view"); + } + EntityView entityViewById = entityViewService.findEntityViewById(tenantId, entityViewId); + if (entityViewById == null) { created = true; - entityView = new EntityView(); - entityView.setTenantId(tenantId); - entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId())); + entityView.setId(null); + } else { + entityView.setId(entityViewId); } + String entityViewName = entityView.getName(); EntityView entityViewByName = entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName); if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); log.warn("[{}] Entity view with name {} already exists. Renaming entity view name to {}", - tenantId, entityViewUpdateMsg.getName(), entityViewName); + tenantId, entityView.getName(), entityViewName); entityViewNameUpdated = true; } entityView.setName(entityViewName); + setCustomerId(tenantId, created ? null : entityViewById.getCustomerId(), entityView, entityViewUpdateMsg, edgeVersion); + + entityViewValidator.validate(entityView, EntityView::getTenantId); + if (created) { + entityView.setId(entityViewId); + } + entityViewService.saveEntityView(entityView, false); + return Pair.of(created, entityViewNameUpdated); + } + + private EntityView createEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg) { + EntityView entityView = new EntityView(); + entityView.setTenantId(tenantId); + entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId())); + entityView.setName(entityViewUpdateMsg.getName()); entityView.setType(entityViewUpdateMsg.getType()); - entityView.setCustomerId(customerId); + entityView.setAdditionalInfo(entityViewUpdateMsg.hasAdditionalInfo() ? JacksonUtil.toJsonNode(entityViewUpdateMsg.getAdditionalInfo()) : null); + CustomerId customerId = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); + entityView.setCustomerId(customerId); + UUID entityIdUUID = safeGetUUID(entityViewUpdateMsg.getEntityIdMSB(), entityViewUpdateMsg.getEntityIdLSB()); if (EdgeEntityType.DEVICE.equals(entityViewUpdateMsg.getEntityType())) { entityView.setEntityId(entityIdUUID != null ? new DeviceId(entityIdUUID) : null); } else if (EdgeEntityType.ASSET.equals(entityViewUpdateMsg.getEntityType())) { entityView.setEntityId(entityIdUUID != null ? new AssetId(entityIdUUID) : null); } - - entityViewValidator.validate(entityView, EntityView::getTenantId); - if (created) { - entityView.setId(entityViewId); - } - entityViewService.saveEntityView(entityView, false); - return Pair.of(created, entityViewNameUpdated); + return entityView; } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg, EdgeVersion edgeVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index cd3a0b35ad..863c1112e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -34,9 +34,12 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @@ -45,7 +48,7 @@ import java.util.UUID; @TbCoreComponent public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { - public ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg) { + public ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processEntityViewMsgFromEdge [{}] from edge [{}]", tenantId, entityViewUpdateMsg, edge.getId()); EntityViewId entityViewId = new EntityViewId(new UUID(entityViewUpdateMsg.getIdMSB(), entityViewUpdateMsg.getIdLSB())); try { @@ -54,7 +57,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { switch (entityViewUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge); + saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge, edgeVersion); return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: EntityView entityViewToDelete = entityViewService.findEntityViewById(tenantId, entityViewId); @@ -78,9 +81,8 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { } } - private void saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, Edge edge) { - CustomerId customerId = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); - Pair resultPair = super.saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, customerId); + private void saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, Edge edge, EdgeVersion edgeVersion) { + Pair resultPair = super.saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edgeVersion); Boolean created = resultPair.getFirst(); if (created) { createRelationFromEdge(tenantId, edge.getId(), entityViewId); @@ -104,7 +106,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { } } - public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -116,8 +118,8 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId); if (entityView != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - EntityViewUpdateMsg entityViewUpdateMsg = - entityViewMsgConstructor.constructEntityViewUpdatedMsg(msgType, entityView); + EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) + entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewUpdatedMsg(msgType, entityView); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addEntityViewUpdateMsg(entityViewUpdateMsg) @@ -126,8 +128,8 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { break; case DELETED: case UNASSIGNED_FROM_EDGE: - EntityViewUpdateMsg entityViewUpdateMsg = - entityViewMsgConstructor.constructEntityViewDeleteMsg(entityViewId); + EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor) + entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewDeleteMsg(entityViewId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addEntityViewUpdateMsg(entityViewUpdateMsg) @@ -136,4 +138,12 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { } return downlinkMsg; } + + @Override + protected void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg, EdgeVersion edgeVersion) { + CustomerId customerUUID = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()) + : entityView.getCustomerId() != null ? entityView.getCustomerId() : customerId; + entityView.setCustomerId(customerUUID); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index fae6399e3a..247cfe5c15 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -22,9 +22,11 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @Component @@ -32,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @TbCoreComponent public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertOtaPackageEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertOtaPackageEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -41,8 +43,8 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { OtaPackage otaPackage = otaPackageService.findOtaPackageById(edgeEvent.getTenantId(), otaPackageId); if (otaPackage != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - OtaPackageUpdateMsg otaPackageUpdateMsg = - otaPackageMsgConstructor.constructOtaPackageUpdatedMsg(msgType, otaPackage); + OtaPackageUpdateMsg otaPackageUpdateMsg = ((OtaPackageMsgConstructor) + otaPackageMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructOtaPackageUpdatedMsg(msgType, otaPackage); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addOtaPackageUpdateMsg(otaPackageUpdateMsg) @@ -50,8 +52,8 @@ public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { } break; case DELETED: - OtaPackageUpdateMsg otaPackageUpdateMsg = - otaPackageMsgConstructor.constructOtaPackageDeleteMsg(otaPackageId); + OtaPackageUpdateMsg otaPackageUpdateMsg = ((OtaPackageMsgConstructor) + otaPackageMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructOtaPackageDeleteMsg(otaPackageId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addOtaPackageUpdateMsg(otaPackageUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index 8562582940..cd595c1699 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -22,17 +22,19 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -@Component @Slf4j +@Component @TbCoreComponent public class QueueEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertQueueEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertQueueEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { QueueId queueId = new QueueId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -41,8 +43,8 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { Queue queue = queueService.findQueueById(edgeEvent.getTenantId(), queueId); if (queue != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - QueueUpdateMsg queueUpdateMsg = - queueMsgConstructor.constructQueueUpdatedMsg(msgType, queue); + QueueUpdateMsg queueUpdateMsg = ((QueueMsgConstructor) + queueMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructQueueUpdatedMsg(msgType, queue); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addQueueUpdateMsg(queueUpdateMsg) @@ -50,8 +52,8 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { } break; case DELETED: - QueueUpdateMsg queueDeleteMsg = - queueMsgConstructor.constructQueueDeleteMsg(queueId); + QueueUpdateMsg queueDeleteMsg = ((QueueMsgConstructor) + queueMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructQueueDeleteMsg(queueId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addQueueUpdateMsg(queueDeleteMsg) @@ -60,5 +62,4 @@ public class QueueEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java index 952affdc3d..d673b1a613 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/BaseRelationProcessor.java @@ -25,41 +25,35 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.UUID; @Slf4j public abstract class BaseRelationProcessor extends BaseEdgeProcessor { - protected ListenableFuture processRelationMsg(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { + protected ListenableFuture processRelationMsg(TenantId tenantId, RelationUpdateMsg relationUpdateMsg, EdgeVersion edgeVersion) { + log.trace("[{}] processRelationMsg [{}]", tenantId, relationUpdateMsg); try { - EntityRelation entityRelation = new EntityRelation(); - - UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); - EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); - entityRelation.setFrom(fromId); - - UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); - EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); - entityRelation.setTo(toId); - - entityRelation.setType(relationUpdateMsg.getType()); - entityRelation.setTypeGroup(relationUpdateMsg.hasTypeGroup() - ? RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup()) : RelationTypeGroup.COMMON); - entityRelation.setAdditionalInfo(JacksonUtil.toJsonNode(relationUpdateMsg.getAdditionalInfo())); + EntityRelation entityRelation = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createEntityRelation(relationUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(relationUpdateMsg.getEntity(), EntityRelation.class); + if (entityRelation == null) { + throw new RuntimeException("[{" + tenantId + "}] relationUpdateMsg {" + relationUpdateMsg + "} cannot be converted to entity relation"); + } switch (relationUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: if (isEntityExists(tenantId, entityRelation.getTo()) && isEntityExists(tenantId, entityRelation.getFrom())) { relationService.saveRelation(tenantId, entityRelation); - break; } else { log.warn("[{}] Skipping relating update msg because from/to entity doesn't exists on edge, {}", tenantId, relationUpdateMsg); - break; } + break; case ENTITY_DELETED_RPC_MESSAGE: relationService.deleteRelation(tenantId, entityRelation); break; @@ -73,4 +67,22 @@ public abstract class BaseRelationProcessor extends BaseEdgeProcessor { } return Futures.immediateFuture(null); } + + private EntityRelation createEntityRelation(RelationUpdateMsg relationUpdateMsg) { + EntityRelation entityRelation = new EntityRelation(); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); + entityRelation.setFrom(fromId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); + entityRelation.setTo(toId); + + entityRelation.setType(relationUpdateMsg.getType()); + entityRelation.setTypeGroup(relationUpdateMsg.hasTypeGroup() + ? RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup()) : RelationTypeGroup.COMMON); + entityRelation.setAdditionalInfo(JacksonUtil.toJsonNode(relationUpdateMsg.getAdditionalInfo())); + return entityRelation; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index b3676e8d82..256d84a1e3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -30,10 +30,12 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructor; import java.util.ArrayList; import java.util.HashSet; @@ -45,21 +47,21 @@ import java.util.Set; @TbCoreComponent public class RelationEdgeProcessor extends BaseRelationProcessor { - public ListenableFuture processRelationMsgFromEdge(TenantId tenantId, Edge edge, RelationUpdateMsg relationUpdateMsg) { + public ListenableFuture processRelationMsgFromEdge(TenantId tenantId, Edge edge, RelationUpdateMsg relationUpdateMsg, EdgeVersion edgeVersion) { log.trace("[{}] executing processRelationMsgFromEdge [{}] from edge [{}]", tenantId, relationUpdateMsg, edge.getId()); try { edgeSynchronizationManager.getEdgeId().set(edge.getId()); - - return processRelationMsg(tenantId, relationUpdateMsg); + return processRelationMsg(tenantId, relationUpdateMsg, edgeVersion); } finally { edgeSynchronizationManager.getEdgeId().remove(); } } - public DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertRelationEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityRelation entityRelation = JacksonUtil.convertValue(edgeEvent.getBody(), EntityRelation.class); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RelationUpdateMsg relationUpdateMsg = relationMsgConstructor.constructRelationUpdatedMsg(msgType, entityRelation); + RelationUpdateMsg relationUpdateMsg = ((RelationMsgConstructor) relationMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRelationUpdatedMsg(msgType, entityRelation); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRelationUpdateMsg(relationUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java index a00745d8b7..fc1b300322 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java @@ -17,6 +17,8 @@ package org.thingsboard.server.service.edge.rpc.processor.resource; import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Base64Utils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; @@ -24,45 +26,45 @@ 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.PageDataIterable; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @Slf4j public abstract class BaseResourceProcessor extends BaseEdgeProcessor { - protected boolean saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) { + protected boolean saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg, EdgeVersion edgeVersion) { boolean resourceKeyUpdated = false; try { - boolean created = false; - TbResource resource = resourceService.findResourceById(tenantId, tbResourceId); + TbResource resource = EdgeVersionUtils.isEdgeVersionOlderThan_3_6_2(edgeVersion) + ? createTbResource(tenantId, resourceUpdateMsg) + : JacksonUtil.fromStringIgnoreUnknownProperties(resourceUpdateMsg.getEntity(), TbResource.class); if (resource == null) { - resource = new TbResource(); - if (resourceUpdateMsg.getIsSystem()) { - resource.setTenantId(TenantId.SYS_TENANT_ID); - } else { - resource.setTenantId(tenantId); - } + throw new RuntimeException("[{" + tenantId + "}] resourceUpdateMsg {" + resourceUpdateMsg + " } cannot be converted to resource"); + } + boolean created = false; + TbResource resourceById = resourceService.findResourceById(tenantId, tbResourceId); + if (resourceById == null) { resource.setCreatedTime(Uuids.unixTimestamp(tbResourceId.getId())); created = true; + resource.setId(null); + } else { + resource.setId(tbResourceId); } - String resourceKey = resourceUpdateMsg.getResourceKey(); - ResourceType resourceType = ResourceType.valueOf(resourceUpdateMsg.getResourceType()); + String resourceKey = resource.getResourceKey(); + ResourceType resourceType = resource.getResourceType(); PageDataIterable resourcesIterable = new PageDataIterable<>( link -> resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, resourceType, link), 1024); for (TbResource tbResource : resourcesIterable) { - if (tbResource.getResourceKey().equals(resourceUpdateMsg.getResourceKey()) && !tbResourceId.equals(tbResource.getId())) { + if (tbResource.getResourceKey().equals(resourceKey) && !tbResourceId.equals(tbResource.getId())) { resourceKey = StringUtils.randomAlphabetic(15) + "_" + resourceKey; log.warn("[{}] Resource with resource type {} and key {} already exists. Renaming resource key to {}", - tenantId, resourceType, resourceUpdateMsg.getResourceKey(), resourceKey); + tenantId, resourceType, resource.getResourceKey(), resourceKey); resourceKeyUpdated = true; } } - resource.setTitle(resourceUpdateMsg.getTitle()); resource.setResourceKey(resourceKey); - resource.setResourceType(resourceType); - resource.setFileName(resourceUpdateMsg.getFileName()); - resource.setData(resourceUpdateMsg.hasData() ? resourceUpdateMsg.getData() : null); - resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null); resourceValidator.validate(resource, TbResourceInfo::getTenantId); if (created) { resource.setId(tbResourceId); @@ -74,4 +76,22 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor { } return resourceKeyUpdated; } + + private TbResource createTbResource(TenantId tenantId, ResourceUpdateMsg resourceUpdateMsg) { + TbResource resource = new TbResource(); + if (resourceUpdateMsg.getIsSystem()) { + resource.setTenantId(TenantId.SYS_TENANT_ID); + } else { + resource.setTenantId(tenantId); + } + resource.setTitle(resourceUpdateMsg.getTitle()); + resource.setResourceKey(resourceUpdateMsg.getResourceKey()); + resource.setResourceType(ResourceType.valueOf(resourceUpdateMsg.getResourceType())); + resource.setFileName(resourceUpdateMsg.getFileName()); + if (resourceUpdateMsg.hasData()) { + resource.setEncodedData(resourceUpdateMsg.getData()); + } + resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null); + return resource; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index ad283826f8..8a91849670 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -29,9 +29,11 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructor; import java.util.UUID; @@ -40,7 +42,7 @@ import java.util.UUID; @TbCoreComponent public class ResourceEdgeProcessor extends BaseResourceProcessor { - public ListenableFuture processResourceMsgFromEdge(TenantId tenantId, Edge edge, ResourceUpdateMsg resourceUpdateMsg) { + public ListenableFuture processResourceMsgFromEdge(TenantId tenantId, Edge edge, ResourceUpdateMsg resourceUpdateMsg, EdgeVersion edgeVersion) { TbResourceId tbResourceId = new TbResourceId(new UUID(resourceUpdateMsg.getIdMSB(), resourceUpdateMsg.getIdLSB())); try { edgeSynchronizationManager.getEdgeId().set(edge.getId()); @@ -48,7 +50,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor { switch (resourceUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - boolean resourceKeyUpdated = super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg); + boolean resourceKeyUpdated = super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg, edgeVersion); if (resourceKeyUpdated) { saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.TB_RESOURCE, EdgeEventActionType.UPDATED, tbResourceId, null); } @@ -58,7 +60,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor { return handleUnsupportedMsgType(resourceUpdateMsg.getMsgType()); } } catch (DataValidationException e) { - if (e.getMessage().contains("files size limit is exhausted")) { + if (e.getMessage().contains("exceeds the maximum")) { log.warn("[{}] Resource data size has been exhausted {}", tenantId, resourceUpdateMsg, e); return Futures.immediateFuture(null); } else { @@ -70,7 +72,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor { return Futures.immediateFuture(null); } - public DownlinkMsg convertResourceEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertResourceEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -79,8 +81,8 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor { TbResource tbResource = resourceService.findResourceById(edgeEvent.getTenantId(), tbResourceId); if (tbResource != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - ResourceUpdateMsg resourceUpdateMsg = - resourceMsgConstructor.constructResourceUpdatedMsg(msgType, tbResource); + ResourceUpdateMsg resourceUpdateMsg = ((ResourceMsgConstructor) + resourceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructResourceUpdatedMsg(msgType, tbResource); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addResourceUpdateMsg(resourceUpdateMsg) @@ -88,8 +90,8 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor { } break; case DELETED: - ResourceUpdateMsg resourceUpdateMsg = - resourceMsgConstructor.constructResourceDeleteMsg(tbResourceId); + ResourceUpdateMsg resourceUpdateMsg = ((ResourceMsgConstructor) + resourceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructResourceDeleteMsg(tbResourceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addResourceUpdateMsg(resourceUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index ba6fc1ef97..c17fb8030c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -28,6 +28,7 @@ import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; @@ -37,7 +38,7 @@ import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService @TbCoreComponent public class RuleChainEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -53,8 +54,9 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { } catch (Exception ignored) {} } UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainUpdateMsg ruleChainUpdateMsg = - ruleChainMsgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); + RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) + ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainUpdateMsg(ruleChainUpdateMsg) @@ -65,7 +67,8 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { case UNASSIGNED_FROM_EDGE: downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(ruleChainMsgConstructor.constructRuleChainDeleteMsg(ruleChainId)) + .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainDeleteMsg(ruleChainId)) .build(); break; } @@ -79,8 +82,9 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor) + ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); if (ruleChainMetadataUpdateMsg != null) { downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index bbd9edd172..6c201a261c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -23,7 +23,9 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @Component @@ -31,16 +33,16 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @TbCoreComponent public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertAdminSettingsEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertAdminSettingsEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class); if (adminSettings == null) { return null; } - AdminSettingsUpdateMsg adminSettingsUpdateMsg = adminSettingsMsgConstructor.constructAdminSettingsUpdateMsg(adminSettings); + AdminSettingsUpdateMsg adminSettingsUpdateMsg = ((AdminSettingsMsgConstructor) + adminSettingsMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAdminSettingsUpdateMsg(adminSettings); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAdminSettingsUpdateMsg(adminSettingsUpdateMsg) .build(); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index 974e9267f6..9cea8050b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edge.rpc.processor.telemetry; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -28,7 +27,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -54,7 +52,8 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; @@ -314,7 +313,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { EntityType entityType, UUID entityUUID, EdgeEventActionType actionType, - JsonNode body) throws JsonProcessingException { + JsonNode body) { EntityId entityId; switch (entityType) { case DEVICE: @@ -349,5 +348,4 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { return bodyJackson == null ? null : entityDataMsgConstructor.constructEntityDataMsg(tenantId, entityId, actionType, JsonParser.parseString(bodyJackson)); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java index 667d661d2e..af9ff3de37 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edge.rpc.processor.telemetry; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DataConstants; @@ -37,7 +36,7 @@ public class TelemetryEdgeProcessor extends BaseTelemetryProcessor { return DataConstants.EDGE_MSG_SOURCE; } - public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException { + public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) { if (edgeEvent.getBody() != null) { String bodyStr = edgeEvent.getBody().toString(); if (bodyStr.length() > 1000) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java index 9fd08b4447..92b887bf33 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @Component @@ -43,9 +44,13 @@ public class TenantEdgeProcessor extends BaseEdgeProcessor { Tenant tenant = tenantService.findTenantById(tenantId); if (tenant != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant); + TenantUpdateMsg tenantUpdateMsg = ((TenantMsgConstructor) + tenantMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructTenantUpdateMsg(msgType, tenant); TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); - TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion); + TenantProfileUpdateMsg tenantProfileUpdateMsg = ((TenantMsgConstructor) + tenantMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addTenantUpdateMsg(tenantUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index 970ea3a1f6..57c29e8632 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @Component @@ -41,8 +42,9 @@ public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId); if (tenantProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - TenantProfileUpdateMsg tenantProfileUpdateMsg = - tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion); + TenantProfileUpdateMsg tenantProfileUpdateMsg = ((TenantMsgConstructor) + tenantMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addTenantProfileUpdateMsg(tenantProfileUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index de40fdb1a3..c07cbc9414 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -23,9 +23,11 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @Component @@ -33,7 +35,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @TbCoreComponent public class UserEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertUserEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertUserEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -44,21 +46,21 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(userMsgConstructor.constructUserUpdatedMsg(msgType, user)) + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) .build(); } break; case DELETED: downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(userMsgConstructor.constructUserDeleteMsg(userId)) + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserDeleteMsg(userId)) .build(); break; case CREDENTIALS_UPDATED: UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { UserCredentialsUpdateMsg userCredentialsUpdateMsg = - userMsgConstructor.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + ((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserCredentialsUpdatedMsg(userCredentialsByUserId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) @@ -68,5 +70,4 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 877ece1b15..28cd3167aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -22,9 +22,11 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import java.util.List; @@ -34,7 +36,7 @@ import java.util.List; @TbCoreComponent public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertWidgetsBundleEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertWidgetsBundleEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -45,7 +47,7 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { List widgets = widgetTypeService.findWidgetFqnsByWidgetsBundleId(edgeEvent.getTenantId(), widgetsBundleId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = - widgetsBundleMsgConstructor.constructWidgetsBundleUpdateMsg(msgType, widgetsBundle, widgets); + ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetsBundleUpdateMsg(msgType, widgetsBundle, widgets); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) @@ -54,7 +56,7 @@ public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { break; case DELETED: WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = - widgetsBundleMsgConstructor.constructWidgetsBundleDeleteMsg(widgetsBundleId); + ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetsBundleDeleteMsg(widgetsBundleId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetsBundleUpdateMsg(widgetsBundleUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index de9617d5d7..40556b5215 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -26,10 +26,11 @@ import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -@Component @Slf4j +@Component @TbCoreComponent public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { @@ -43,7 +44,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { if (widgetTypeDetails != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); WidgetTypeUpdateMsg widgetTypeUpdateMsg = - widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails, edgeVersion); + ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails, edgeVersion); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) @@ -52,7 +53,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { break; case DELETED: WidgetTypeUpdateMsg widgetTypeUpdateMsg = - widgetTypeMsgConstructor.constructWidgetTypeDeleteMsg(widgetTypeId); + ((WidgetMsgConstructor) widgetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructWidgetTypeDeleteMsg(widgetTypeId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) @@ -61,5 +62,4 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { } return downlinkMsg; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index af9f72e819..cd23f19556 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -294,7 +294,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); - query.setParameters(new RelationsSearchParameters(entityId, direction, -1, false)); + query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); return relationService.findByQuery(tenantId, query); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index c9e6987a23..dc66e50748 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -24,4 +24,8 @@ public final class EdgeVersionUtils { public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { return currentVersion.ordinal() < requiredVersion.ordinal(); } + + public static boolean isEdgeVersionOlderThan_3_6_2(EdgeVersion currentVersion) { + return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_6_2); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 72e8f44020..8682d18a62 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -112,7 +112,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS public void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, User user, Object... additionalInfo) { gatewayNotificationsService.onDeviceDeleted(device); - tbClusterService.onDeviceDeleted(device, null); + tbClusterService.onDeviceDeleted(tenantId, device, null); logEntityAction(tenantId, deviceId, device, customerId, ActionType.DELETED, user, additionalInfo); } @@ -126,6 +126,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS @Override public void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId, Device device, Tenant tenant, User user, Object... additionalInfo) { + tbClusterService.onDeviceAssignedToTenant(tenantId, device); logEntityAction(tenantId, deviceId, device, customerId, ActionType.ASSIGNED_TO_TENANT, user, additionalInfo); pushAssignedFromNotification(tenant, newTenantId, device); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index 70918486a3..8d143d7607 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 19d4126989..3267ed1c17 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -54,6 +54,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T if (created) { installScripts.createDefaultRuleChains(savedTenant.getId()); installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); + installScripts.createDefaultTenantDashboards(savedTenant.getId(), null); } tenantProfileCache.evict(savedTenant.getId()); notificationEntityService.notifyCreateOrUpdateTenant(savedTenant, created ? diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java index 1d40e9630c..53afc9fb4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy.widgets.type; import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -33,6 +35,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @AllArgsConstructor public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService { + private final WidgetTypeService widgetTypeService; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java new file mode 100644 index 0000000000..74e1222acf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.executors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ExecutorProvider; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.queue.util.TbRuleEngineComponent; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Lazy +@TbRuleEngineComponent +@Component +public class PubSubRuleNodeExecutorProvider implements ExecutorProvider { + + @Value("${service.rule_engine.pubsub.executor_thread_pool_size}") + private Integer threadPoolSize; + + /** + * Refers to com.google.cloud.pubsub.v1.Publisher default executor configuration + */ + private static final int THREADS_PER_CPU = 5; + private ScheduledExecutorService executor; + + @PostConstruct + public void init() { + if (threadPoolSize == null) { + threadPoolSize = THREADS_PER_CPU * Runtime.getRuntime().availableProcessors(); + } + executor = Executors.newScheduledThreadPool(threadPoolSize, ThingsBoardThreadFactory.forName("pubsub-rule-nodes")); + } + + @Override + public ScheduledExecutorService getExecutor() { + return executor; + } + + @PreDestroy + private void destroy() { + if (executor != null) { + executor.shutdownNow(); + } + } +} 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 cf7b779dbe..63771f8165 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 @@ -530,6 +530,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 30)))); installScripts.loadDashboards(demoTenant.getId(), null); + installScripts.createDefaultTenantDashboards(demoTenant.getId(), null); } @Override 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 3e23f9af37..598c3c8965 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 @@ -16,14 +16,15 @@ package org.thingsboard.server.service.install; import com.fasterxml.jackson.databind.JsonNode; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -44,6 +45,7 @@ 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; +import org.thingsboard.server.service.install.update.ImagesUpdater; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -51,12 +53,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Stream; import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource; @@ -109,6 +111,9 @@ public class InstallScripts { @Autowired private ResourceService resourceService; + @Autowired + private ImagesUpdater imagesUpdater; + Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } @@ -153,16 +158,14 @@ public class InstallScripts { } private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { - findRuleChainsFromPath(ruleChainsPath) - .forEach( - path -> { - try { - createRuleChainFromFile(tenantId, path, null); - } catch (Exception e) { - log.error("Unable to load rule chain from json: [{}]", path.toString()); - throw new RuntimeException("Unable to load rule chain from json", e); - } - }); + findRuleChainsFromPath(ruleChainsPath).forEach(path -> { + try { + createRuleChainFromFile(tenantId, path, null); + } catch (Exception e) { + log.error("Unable to load rule chain from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load rule chain from json", e); + } + }); } List findRuleChainsFromPath(Path ruleChainsPath) throws IOException { @@ -195,6 +198,7 @@ public class InstallScripts { } public void loadSystemWidgets() throws Exception { + log.info("Loading system widgets"); Map widgetsBundlesMap = new HashMap<>(); Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -297,8 +301,41 @@ public class InstallScripts { } } + public void updateImages() { + imagesUpdater.updateWidgetsBundlesImages(); + imagesUpdater.updateWidgetTypesImages(); + imagesUpdater.updateDashboardsImages(); + imagesUpdater.updateDeviceProfilesImages(); + imagesUpdater.updateAssetProfilesImages(); + } + + @SneakyThrows + public void loadSystemImages() { + log.info("Loading system images..."); + Stream dashboardsFiles = Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)); + try (dashboardsFiles) { + dashboardsFiles.forEach(file -> { + try { + Dashboard dashboard = JacksonUtil.OBJECT_MAPPER.readValue(file.toFile(), Dashboard.class); + imagesUpdater.createSystemImages(dashboard); + } catch (Exception e) { + log.error("Failed to create system images for default dashboard {}", file.getFileName(), e); + } + }); + } + } + public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); + loadDashboardsFromDir(tenantId, customerId, dashboardsDir); + } + + public void createDefaultTenantDashboards(TenantId tenantId, CustomerId customerId) throws Exception { + Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR); + loadDashboardsFromDir(tenantId, customerId, dashboardsDir); + } + + private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) throws IOException { try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( path -> { @@ -359,7 +396,7 @@ public class InstallScripts { dirStream.forEach( path -> { try { - String data = Base64.getEncoder().encodeToString(Files.readAllBytes(path)); + byte[] data = Files.readAllBytes(path); TbResource tbResource = new TbResource(); tbResource.setTenantId(TenantId.SYS_TENANT_ID); tbResource.setData(data); @@ -380,15 +417,13 @@ public class InstallScripts { private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException { log.trace("Executing saveResource [{}]", resource); - if (StringUtils.isEmpty(resource.getData())) { + if (resource.getData() == null || resource.getData().length == 0) { throw new DataValidationException("Resource data should be specified!"); } toLwm2mResource(resource); - TbResource foundResource = - resourceService.getResource(TenantId.SYS_TENANT_ID, ResourceType.LWM2M_MODEL, resource.getResourceKey()); + TbResource foundResource = resourceService.findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, ResourceType.LWM2M_MODEL, resource.getResourceKey()); if (foundResource == null) { resourceService.saveResource(resource); } } } - diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index c02b7e9cc1..a4e9d61a74 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -63,6 +63,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; @@ -717,86 +718,73 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService } break; case "3.5.0": - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - log.info("Updating schema ..."); - if (isOldSchema(conn, 3005000)) { - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.0", SCHEMA_UPDATE_SQL); - loadSql(schemaUpdateFile, conn); - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005001;"); - } - log.info("Schema updated."); - } catch (Exception e) { - log.error("Failed updating schema!!!", e); - } + updateSchema("3.5.0", 3005000, "3.5.1", 3005001, null); break; case "3.5.1": - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - if (isOldSchema(conn, 3005001)) { - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL); - loadSql(schemaUpdateFile, conn); - - String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package", - "asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"}; - for (String entityName : entityNames) { - try { - conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE"); - } catch (Exception e) { - } - } + updateSchema("3.5.1", 3005001, "3.6.0", 3006000, conn -> { + String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package", + "asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"}; + for (String entityName : entityNames) { try { - conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;"); + conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE"); } catch (Exception e) { } - try { - conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;"); - } catch (Exception e) { - } - try { - conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); - } catch (Exception e) { - } - try { - conn.createStatement().execute("UPDATE rule_node SET " + - "configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " + - "configuration_version = 1 " + - "WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;"); - } catch (Exception e) { - } - try { - conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); - } catch (Exception e) { - } - - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3006000;"); - log.info("Schema updated to version 3.6.0."); - } else { - log.info("Skip schema re-update to version 3.6.0. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update."); } - } catch (Exception e) { - log.error("Failed updating schema!!!", e); - } + try { + conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;"); + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;"); + } catch (Exception e) { + } + try { + conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); + } catch (Exception e) { + } + try { + conn.createStatement().execute("UPDATE rule_node SET " + + "configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " + + "configuration_version = 1 " + + "WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;"); + } catch (Exception e) { + } + try { + conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); + } catch (Exception e) { + } + }); break; case "3.6.0": - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - if (isOldSchema(conn, 3006000)) { - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.6.0", SCHEMA_UPDATE_SQL); - loadSql(schemaUpdateFile, conn); - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3006001;"); - log.info("Schema updated to version 3.6.1."); - } else { - log.info("Skip schema re-update to version 3.6.1. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update."); - } - } catch (Exception e) { - log.error("Failed updating schema!!!", e); - } + updateSchema("3.6.0", 3006000, "3.6.1", 3006001, null); + break; + case "3.6.1": + updateSchema("3.6.1", 3006001, "3.6.2", 3006002, null); break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } } + private void updateSchema(String oldVersionStr, int oldVersion, String newVersionStr, int newVersion, Consumer additionalAction) { + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, oldVersion)) { + Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", oldVersionStr, SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + if (additionalAction != null) { + additionalAction.accept(conn); + } + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = " + newVersion + ";"); + log.info("Schema updated to version {}", newVersionStr); + } else { + log.info("Skip schema re-update to version {}. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.", newVersionStr); + } + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + } + private void runSchemaUpdateScript(Connection connection, String version) throws Exception { Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, connection); diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java new file mode 100644 index 0000000000..e711bf2682 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install.update; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.asset.AssetProfileDao; +import org.thingsboard.server.dao.dashboard.DashboardDao; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.dao.widget.WidgetTypeDao; +import org.thingsboard.server.dao.widget.WidgetsBundleDao; + +import java.util.function.BiFunction; +import java.util.function.Function; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ImagesUpdater { + private final ImageService imageService; + private final WidgetsBundleDao widgetsBundleDao; + private final WidgetTypeDao widgetTypeDao; + private final DashboardDao dashboardDao; + private final DeviceProfileDao deviceProfileDao; + private final AssetProfileDao assetProfileDao; + + public void updateWidgetsBundlesImages() { + log.info("Updating widgets bundles images..."); + var widgetsBundles = new PageDataIterable<>(widgetsBundleDao::findAllWidgetsBundles, 128); + updateImages(widgetsBundles, "bundle", imageService::replaceBase64WithImageUrl, widgetsBundleDao); + } + + public void updateWidgetTypesImages() { + log.info("Updating widget types images..."); + var widgetTypesIds = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024); + updateImages(widgetTypesIds, "widget type", imageService::replaceBase64WithImageUrl, widgetTypeDao); + } + + public void updateDashboardsImages() { + log.info("Updating dashboards images..."); + var dashboardsIds = new PageDataIterable<>(dashboardDao::findAllIds, 1024); + updateImages(dashboardsIds, "dashboard", imageService::replaceBase64WithImageUrl, dashboardDao); + } + + public void createSystemImages(Dashboard defaultDashboard) { + defaultDashboard.setTenantId(TenantId.SYS_TENANT_ID); + boolean created = imageService.replaceBase64WithImageUrl(defaultDashboard); + if (created) { + log.debug("Created system images for default dashboard '{}'", defaultDashboard.getTitle()); + } + } + + public void updateDeviceProfilesImages() { + log.info("Updating device profiles images..."); + var deviceProfiles = new PageDataIterable<>(deviceProfileDao::findAllWithImages, 256); + updateImages(deviceProfiles, "device profile", imageService::replaceBase64WithImageUrl, deviceProfileDao); + } + + public void updateAssetProfilesImages() { + log.info("Updating asset profiles images..."); + var assetProfiles = new PageDataIterable<>(assetProfileDao::findAllWithImages, 256); + updateImages(assetProfiles, "asset profile", imageService::replaceBase64WithImageUrl, assetProfileDao); + } + + private void updateImages(Iterable entities, String type, + BiFunction updater, Dao dao) { + int updatedCount = 0; + int totalCount = 0; + for (E entity : entities) { + totalCount++; + try { + boolean updated = updater.apply(entity, type); + if (updated) { + dao.save(entity.getTenantId(), entity); + log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type); + updatedCount++; + } + } catch (Exception e) { + log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e); + } + if (totalCount % 100 == 0) { + log.info("Processed {} {}s so far", totalCount, type); + } + } + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + } + + private void updateImages(Iterable entitiesIds, String type, + Function updater, Dao dao) { + int updatedCount = 0; + int totalCount = 0; + for (EntityId id : entitiesIds) { + totalCount++; + E entity = dao.findById(TenantId.SYS_TENANT_ID, id.getId()); + try { + boolean updated = updater.apply(entity); + if (updated) { + dao.save(entity.getTenantId(), entity); + log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type); + updatedCount++; + } + } catch (Exception e) { + log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e); + } + if (totalCount % 100 == 0) { + log.info("Processed {} {}s so far", totalCount, type); + } + } + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index f96e49930c..14930912d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -81,7 +81,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import static org.thingsboard.server.service.queue.ProtoUtils.toProto; +import static org.thingsboard.server.common.util.ProtoUtils.toProto; @Service @Slf4j @@ -308,10 +308,17 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onDeviceDeleted(Device device, TbQueueCallback callback) { - broadcastEntityDeleteToTransport(device.getTenantId(), device.getId(), device.getName(), callback); - sendDeviceStateServiceEvent(device.getTenantId(), device.getId(), false, false, true); - broadcastEntityStateChangeEvent(device.getTenantId(), device.getId(), ComponentLifecycleEvent.DELETED); + public void onDeviceDeleted(TenantId tenantId, Device device, TbQueueCallback callback) { + DeviceId deviceId = device.getId(); + broadcastEntityDeleteToTransport(tenantId, deviceId, device.getName(), callback); + sendDeviceStateServiceEvent(tenantId, deviceId, false, false, true); + broadcastEntityStateChangeEvent(tenantId, deviceId, ComponentLifecycleEvent.DELETED); + } + + @Override + public void onDeviceAssignedToTenant(TenantId oldTenantId, Device device) { + onDeviceDeleted(oldTenantId, device, null); + sendDeviceStateServiceEvent(device.getTenantId(), device.getId(), true, false, false); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 754537ef11..5283713126 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -82,6 +83,8 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; +import org.thingsboard.server.dao.resource.ImageCacheKey; +import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; @@ -140,6 +143,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; private final TbQueueConsumer> firmwareStatesConsumer; + private final TbImageService imageService; protected volatile ExecutorService consumersExecutor; protected volatile ExecutorService usageStatsExecutor; @@ -165,7 +169,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService jwtSettingsService, NotificationSchedulerService notificationSchedulerService, - NotificationRuleProcessor notificationRuleProcessor) { + NotificationRuleProcessor notificationRuleProcessor, + TbImageService imageService) { super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); @@ -181,6 +186,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService etagCache; + + public DefaultTbImageService(TbClusterService clusterService, ImageService imageService, + @Value("${cache.image.etag.timeToLiveInMinutes:44640}") int cacheTtl, + @Value("${cache.image.etag.maxSize:10000}") int cacheMaxSize) { + this.clusterService = clusterService; + this.imageService = imageService; + this.etagCache = Caffeine.newBuilder() + .expireAfterAccess(cacheTtl, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build(); + } + + @Override + public String getETag(ImageCacheKey imageCacheKey) { + return etagCache.getIfPresent(imageCacheKey); + } + + @Override + public void putETag(ImageCacheKey imageCacheKey, String etag) { + etagCache.put(imageCacheKey, etag); + } + + @Override + public void evictETag(ImageCacheKey imageCacheKey) { + etagCache.invalidate(imageCacheKey); + } + + @Override + public TbResourceInfo save(TbResource image, User user) throws Exception { + ActionType actionType = image.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = image.getTenantId(); + try { + var oldEtag = getEtag(image); + if (image.getId() == null && StringUtils.isNotEmpty(image.getResourceKey())) { + var existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey()); + if (existingImage != null) { + image.setId(existingImage.getId()); + } + } + TbResourceInfo savedImage = imageService.saveImage(image); + notificationEntityService.logEntityAction(tenantId, savedImage.getId(), savedImage, actionType, user); + if (oldEtag.isPresent()) { + var newEtag = getEtag(savedImage); + if (newEtag.isPresent() && !oldEtag.get().equals(newEtag.get())) { + evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), false)); + evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), true)); + clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder() + .setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setResourceKey(image.getResourceKey()) + .build()) + .build()); + } + } + return savedImage; + } catch (Exception e) { + image.setData(null); + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), image, actionType, user, e); + throw e; + } + } + + private Optional getEtag(TbResourceInfo image) throws JsonProcessingException { + var descriptor = image.getDescriptor(ImageDescriptor.class); + return Optional.ofNullable(descriptor != null ? descriptor.getEtag() : null); + } + + private Optional getPreviewEtag(TbResourceInfo image) throws JsonProcessingException { + var descriptor = image.getDescriptor(ImageDescriptor.class); + descriptor = descriptor != null ? descriptor.getPreviewDescriptor() : null; + return Optional.ofNullable(descriptor != null ? descriptor.getEtag() : null); + } + + @Override + public TbResourceInfo save(TbResourceInfo imageInfo, User user) { + TenantId tenantId = imageInfo.getTenantId(); + TbResourceId imageId = imageInfo.getId(); + try { + imageInfo = imageService.saveImageInfo(imageInfo); + notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user); + return imageInfo; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user, e); + throw e; + } + } + + @Override + public TbImageDeleteResult delete(TbResourceInfo imageInfo, User user, boolean force) { + TenantId tenantId = imageInfo.getTenantId(); + TbResourceId imageId = imageInfo.getId(); + try { + TbImageDeleteResult result = imageService.deleteImage(imageInfo, force); + if (result.isSuccess()) { + notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString()); + } + return result; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, imageId, ActionType.DELETED, user, e, imageId.toString()); + throw e; + } + } +} 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 index 064511abd9..2705092b04 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -15,34 +15,24 @@ */ package org.thingsboard.server.service.resource; -import com.google.common.hash.HashCode; -import com.google.common.hash.Hashing; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.TbResourceInfo; -import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.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 org.thingsboard.server.common.data.widget.BaseWidgetType; -import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; -import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import java.util.Base64; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -56,39 +46,51 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource @Slf4j @Service @TbCoreComponent +@RequiredArgsConstructor public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService { private final ResourceService resourceService; - private final WidgetTypeService widgetTypeService; - - public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) { - this.resourceService = resourceService; - this.widgetTypeService = widgetTypeService; - } - - @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(TbResourceInfoFilter filter, PageLink pageLink) { - return resourceService.findAllTenantResourcesByTenantId(filter, pageLink); + public TbResource save(TbResource resource, User user) throws ThingsboardException { + if (resource.getResourceType() == ResourceType.IMAGE) { + throw new IllegalArgumentException("Image resource type is not supported"); + } + ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = resource.getTenantId(); + try { + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + toLwm2mResource(resource); + } else if (resource.getResourceKey() == null) { + resource.setResourceKey(resource.getFileName()); + } + TbResource savedResource = resourceService.saveResource(resource); + tbClusterService.onResourceChange(savedResource, null); + notificationEntityService.logEntityAction(tenantId, savedResource.getId(), savedResource, actionType, user); + return savedResource; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), + resource, actionType, user, e); + throw e; + } } @Override - public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { - return resourceService.findTenantResourcesByTenantId(filter, pageLink); + public void delete(TbResource tbResource, User user) { + if (tbResource.getResourceType() == ResourceType.IMAGE) { + throw new IllegalArgumentException("Image resource type is not supported"); + } + TbResourceId resourceId = tbResource.getId(); + TenantId tenantId = tbResource.getTenantId(); + try { + resourceService.deleteResource(tenantId, resourceId); + tbClusterService.onResourceDeleted(tbResource, null); + notificationEntityService.logEntityAction(tenantId, resourceId, tbResource, ActionType.DELETED, user, resourceId.toString()); + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), + ActionType.DELETED, user, e, resourceId.toString()); + throw e; + } } @Override @@ -114,16 +116,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements .collect(Collectors.toList()); } - @Override - public void deleteResourcesByTenantId(TenantId tenantId) { - resourceService.deleteResourcesByTenantId(tenantId); - } - - @Override - public long sumDataSizeByTenantId(TenantId tenantId) { - return resourceService.sumDataSizeByTenantId(tenantId); - } - private Comparator getComparator(String sortProperty, String sortOrder) { Comparator comparator; if ("name".equals(sortProperty)) { @@ -134,50 +126,4 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; } - @Override - public TbResource save(TbResource tbResource, User user) throws ThingsboardException { - ActionType actionType = tbResource.getId() == null ? ActionType.ADDED : ActionType.UPDATED; - TenantId tenantId = tbResource.getTenantId(); - try { - TbResource savedResource = checkNotNull(doSave(tbResource)); - tbClusterService.onResourceChange(savedResource, null); - notificationEntityService.logEntityAction(tenantId, savedResource.getId(), savedResource, actionType, user); - return savedResource; - } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), - tbResource, actionType, user, e); - throw e; - } - } - - @Override - public void delete(TbResource tbResource, User user) { - TbResourceId resourceId = tbResource.getId(); - TenantId tenantId = tbResource.getTenantId(); - try { - resourceService.deleteResource(tenantId, resourceId); - tbClusterService.onResourceDeleted(tbResource, null); - notificationEntityService.logEntityAction(tenantId, resourceId, tbResource, ActionType.DELETED, user, resourceId.toString()); - } catch (Exception e) { - notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), - ActionType.DELETED, user, e, resourceId.toString()); - throw e; - } - } - - private TbResource doSave(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())) { - toLwm2mResource(resource); - } else { - resource.setResourceKey(resource.getFileName()); - } - HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes())); - resource.setEtag(hashCode.toString()); - return resourceService.saveResource(resource); - } } - diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java new file mode 100644 index 0000000000..c29cc6ad3b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.resource; + +import org.thingsboard.server.common.data.TbImageDeleteResult; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.dao.resource.ImageCacheKey; + +public interface TbImageService { + + TbResourceInfo save(TbResource image, User user) throws Exception; + + TbResourceInfo save(TbResourceInfo imageInfo, User user); + + TbImageDeleteResult delete(TbResourceInfo imageInfo, User user, boolean force); + + String getETag(ImageCacheKey imageCacheKey); + + void putETag(ImageCacheKey imageCacheKey, String etag); + + void evictETag(ImageCacheKey imageCacheKey); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 40fe7dda9a..e6dbc0f071 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -15,14 +15,9 @@ */ package org.thingsboard.server.service.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.TbResourceInfoFilter; -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 org.thingsboard.server.service.entitiy.SimpleTbEntityService; @@ -30,16 +25,6 @@ import java.util.List; public interface TbResourceService extends SimpleTbEntityService { - TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey); - - TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); - - TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); - - PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); - - PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); - List findLwM2mObject(TenantId tenantId, String sortOrder, String sortProperty, @@ -50,7 +35,4 @@ public interface TbResourceService extends SimpleTbEntityService { String sortOrder, PageLink pageLink); - void deleteResourcesByTenantId(TenantId tenantId); - - long sumDataSizeByTenantId(TenantId tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java index d63f759dd3..98ccb2c1be 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -18,11 +18,13 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbCoreComponent; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component +@TbCoreComponent public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; public static final String PREV_URI_PARAMETER = "prevUri"; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java index 3358e5182f..d112075a51 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java @@ -118,6 +118,16 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi } } + @Override + public PageData findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) { + ExportableEntityDao dao = getExportableEntityDao(entityType); + if (dao != null) { + return dao.findIdsByTenantId(tenantId.getId(), pageLink); + } else { + return new PageData<>(); + } + } + @Override public I getExternalIdByInternal(I internalId) { ExportableEntityDao dao = getExportableEntityDao(internalId.getEntityType()); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java index c5c26c4476..eee102961d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java @@ -35,6 +35,8 @@ public interface ExportableEntitiesService { , I extends EntityId> PageData findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink); + PageData findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink); + I getExternalIdByInternal(I internalId); void removeById(TenantId tenantId, I id); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/ResourceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/ResourceExportService.java new file mode 100644 index 0000000000..45e21f5fb4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/ResourceExportService.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Set; + +@Service +@TbCoreComponent +public class ResourceExportService extends BaseEntityExportService> { + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.TB_RESOURCE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index c03dd8dcb7..30765bd959 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -47,7 +47,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumn import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.controller.BaseController; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.action.EntityActionService; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java new file mode 100644 index 0000000000..bc7db18e80 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.User; +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.sync.ie.EntityExportData; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class ResourceImportService extends BaseEntityImportService> { + + private final ResourceService resourceService; + + @Override + protected void setOwner(TenantId tenantId, TbResource resource, IdProvider idProvider) { + resource.setTenantId(tenantId); + } + + @Override + protected TbResource prepare(EntitiesImportCtx ctx, TbResource resource, TbResource oldResource, EntityExportData exportData, IdProvider idProvider) { + return resource; + } + + @Override + protected TbResource findExistingEntity(EntitiesImportCtx ctx, TbResource resource, IdProvider idProvider) { + TbResource existingResource = super.findExistingEntity(ctx, resource, idProvider); + if (existingResource == null && ctx.isFindExistingByName()) { + existingResource = resourceService.findResourceByTenantIdAndKey(ctx.getTenantId(), resource.getResourceType(), resource.getResourceKey()); + } + return existingResource; + } + + @Override + protected boolean compare(EntitiesImportCtx ctx, EntityExportData exportData, TbResource prepared, TbResource existing) { + return true; + } + + @Override + protected TbResource deepCopy(TbResource resource) { + return new TbResource(resource); + } + + @Override + protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData exportData, IdProvider idProvider) { + return resourceService.saveResource(resource); + } + + @Override + protected void onEntitySaved(User user, TbResource savedResource, TbResource oldResource) throws ThingsboardException { + super.onEntitySaved(user, savedResource, oldResource); + clusterService.onResourceChange(savedResource, null); + } + + @Override + public EntityType getEntityType() { + return EntityType.TB_RESOURCE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java index 88ebbbcee3..78cd9a66f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java @@ -20,13 +20,9 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.sync.ie.WidgetTypeExportData; -import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 89d5153a81..f98a2ad780 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -208,10 +208,10 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } if (config.isAllEntities()) { - DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink) - , 100, entity -> { + DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesIdsByTenantId(ctx.getTenantId(), entityType, pageLink), + 100, entityId -> { try { - ctx.add(saveEntityData(ctx, entity.getId())); + ctx.add(saveEntityData(ctx, entityId)); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index df3a15e765..e66029a8a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -175,20 +175,21 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback) { ListenableFuture saveFuture = tsService.save(tenantId, entityId, ts, ttl); - addCallbacks(tenantId, entityId, ts, callback, saveFuture); + addMainCallback(saveFuture, callback); + addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); + addEntityViewCallback(tenantId, entityId, ts); } private void saveWithoutLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, long ttl, FutureCallback callback) { ListenableFuture saveFuture = tsService.saveWithoutLatest(tenantId, entityId, ts, ttl); - addCallbacks(tenantId, entityId, ts, callback, saveFuture); - } - - private void addCallbacks(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback, ListenableFuture saveFuture) { addMainCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); + } + + private void addEntityViewCallback(TenantId tenantId, EntityId entityId, List ts) { if (EntityType.DEVICE.equals(entityId.getEntityType()) || EntityType.ASSET.equals(entityId.getEntityType())) { Futures.addCallback(this.tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId), - new FutureCallback>() { + new FutureCallback<>() { @Override public void onSuccess(@Nullable List result) { if (result != null && !result.isEmpty()) { @@ -212,7 +213,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } } if (!entityViewLatest.isEmpty()) { - saveLatestAndNotify(tenantId, entityView.getId(), entityViewLatest, new FutureCallback() { + saveLatestAndNotify(tenantId, entityView.getId(), entityViewLatest, new FutureCallback<>() { @Override public void onSuccess(@Nullable Void tmp) { } 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 3505052fca..f622b1fee0 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 @@ -78,9 +78,11 @@ 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.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; +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; @@ -144,7 +146,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 OtaPackageService otaPackageService; private final OtaPackageDataCache otaPackageDataCache; private final QueueService queueService; @@ -164,6 +166,7 @@ public class DefaultTransportApiService implements TransportApiService { public void init() { handlerExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(maxCoreHandlerThreads, "transport-api-service-core-handler")); } + @PreDestroy public void destroy() { if (handlerExecutor != null) { @@ -400,6 +403,13 @@ public class DefaultTransportApiService implements TransportApiService { } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); + } catch (EntitiesLimitException e) { + log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage()); + return TransportApiResponseMsg.newBuilder() + .setGetOrCreateDeviceResponseMsg( + GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() + .setError(TransportProtos.TransportApiRequestErrorCode.ENTITY_LIMIT)) + .build(); } finally { deviceCreationLock.unlock(); } @@ -511,10 +521,10 @@ public class DefaultTransportApiService implements TransportApiService { ResourceType resourceType = ResourceType.valueOf(requestMsg.getResourceType()); String resourceKey = requestMsg.getResourceKey(); TransportProtos.GetResourceResponseMsg.Builder builder = TransportProtos.GetResourceResponseMsg.newBuilder(); - TbResource resource = resourceService.getResource(tenantId, resourceType, resourceKey); + TbResource resource = resourceService.findResourceByTenantIdAndKey(tenantId, resourceType, resourceKey); if (resource == null && !tenantId.equals(TenantId.SYS_TENANT_ID)) { - resource = resourceService.getResource(TenantId.SYS_TENANT_ID, resourceType, resourceKey); + resource = resourceService.findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey); } if (resource != null) { diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index 0deb2f6bb7..b73ba09541 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -39,13 +39,13 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA @Slf4j public class LwM2mObjectModelUtils { - + private static final TbDDFFileParser ddfFileParser = new TbDDFFileParser(); - - public static void toLwm2mResource (TbResource resource) throws ThingsboardException { + + public static void toLwm2mResource(TbResource resource) throws ThingsboardException { try { List objectModels = - ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText()); if (!objectModels.isEmpty()) { ObjectModel objectModel = objectModels.get(0); @@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils { public static LwM2mObject toLwM2mObject(TbResource resource, boolean isSave) { try { List objectModels = - ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText()); if (objectModels.size() == 0) { return null; } else { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bdf0ad8252..93348d02ee 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -583,6 +583,15 @@ cache: rateLimits: timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:120}" # Rate limits cache TTL maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:200000}" # 0 means the cache is disabled + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled + image: + etag: + timeToLiveInMinutes: "${CACHE_SPECS_IMAGE_ETAGS_TTL:44640}" # Image ETags cache TTL + maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled + systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled + tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. @@ -875,7 +884,7 @@ transport: ip_limits_enabled: "${TB_TRANSPORT_IP_RATE_LIMITS_ENABLED:false}" # Maximum number of connect attempts with invalid credentials max_wrong_credentials_per_ip: "${TB_TRANSPORT_MAX_WRONG_CREDENTIALS_PER_IP:10}" - # Timeout to expire block IP addresses + # Timeout (in milliseconds) to expire block IP addresses ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" # Local HTTP transport parameters http: @@ -1411,6 +1420,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If set to 0 - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consumed again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" @@ -1639,6 +1650,9 @@ service: # Comma-separated list of tenant profile ids assigned to this Rule Engine. # This Rule Engine will only be responsible for tenants with these profiles (in case 'isolation' option is enabled in the profile). assigned_tenant_profiles: "${TB_RULE_ENGINE_ASSIGNED_TENANT_PROFILES:}" + pubsub: + # Thread pool size for pubsub rule node executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" # Metrics parameters metrics: diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 151e779ae0..76186e6566 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -25,6 +25,7 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.hibernate.exception.ConstraintViolationException; @@ -177,6 +178,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} */ private static final long DEFAULT_TIMEOUT = -1L; + private static final int CLEANUP_TENANT_RETRIES_COUNT = 3; protected MediaType contentType = MediaType.APPLICATION_JSON; @@ -330,10 +332,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { log.debug("Executing web test teardown"); loginSysAdmin(); - doDelete("/api/tenant/" + tenantId.getId().toString()) - .andExpect(status().isOk()); + deleteTenant(tenantId); deleteDifferentTenant(); - verifyNoTenantsLeft(); tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID); @@ -341,7 +341,34 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { log.info("Executed web test teardown"); } - void verifyNoTenantsLeft() throws Exception { + private void verifyNoTenantsLeft() throws Exception { + List loadedTenants = getAllTenants(); + if (!loadedTenants.isEmpty()) { + loadedTenants.forEach(tenant -> deleteTenant(tenant.getId())); + loadedTenants = getAllTenants(); + } + assertThat(loadedTenants).as("All tenants expected to be deleted, but some tenants left in the database").isEmpty(); + } + + private void deleteTenant(TenantId tenantId) { + int status = 0; + int retries = 0; + while (status != HttpStatus.SC_OK && retries < CLEANUP_TENANT_RETRIES_COUNT) { + retries++; + try { + status = doDelete("/api/tenant/" + tenantId.getId().toString()) + .andReturn().getResponse().getStatus(); + if (status != HttpStatus.SC_OK) { + log.warn("Tenant deletion failed, tenantId: {}", tenantId.getId().toString()); + Thread.sleep(1000L); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + private List getAllTenants() throws Exception { List loadedTenants = new ArrayList<>(); PageLink pageLink = new PageLink(10); PageData pageData; @@ -353,8 +380,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - - assertThat(loadedTenants).as("All tenants expected to be deleted, but some tenants left in the database").isEmpty(); + return loadedTenants; } protected void loginSysAdmin() throws Exception { @@ -465,8 +491,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void deleteDifferentTenant() throws Exception { if (savedDifferentTenant != null) { loginSysAdmin(); - doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) - .andExpect(status().isOk()); + deleteTenant(savedDifferentTenant.getId()); savedDifferentTenant = null; } } @@ -777,7 +802,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } - protected T doPut(String urlTemplate, T content, Class responseClass, String... params) { + protected T doPut(String urlTemplate, Object content, Class responseClass, String... params) { try { return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass); } catch (Exception e) { diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 6f0d6f9465..fb8064d6d5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -327,7 +327,18 @@ public class DashboardControllerTest extends AbstractControllerTest { @Test public void testFindTenantDashboards() throws Exception { - List dashboards = new ArrayList<>(); + List expectedDashboards = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", + new TypeReference>() { + }, pageLink); + expectedDashboards.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); Mockito.reset(tbClusterService, auditLogService); @@ -335,7 +346,7 @@ public class DashboardControllerTest extends AbstractControllerTest { for (int i = 0; i < cntEntity; i++) { Dashboard dashboard = new Dashboard(); dashboard.setTitle("Dashboard" + i); - dashboards.add(new DashboardInfo(doPost("/api/dashboard", dashboard, Dashboard.class))); + expectedDashboards.add(new DashboardInfo(doPost("/api/dashboard", dashboard, Dashboard.class))); } testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Dashboard(), new Dashboard(), @@ -343,8 +354,6 @@ public class DashboardControllerTest extends AbstractControllerTest { ActionType.ADDED, cntEntity, cntEntity, cntEntity); List loadedDashboards = new ArrayList<>(); - PageLink pageLink = new PageLink(24); - PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", new TypeReference>() { @@ -355,10 +364,10 @@ public class DashboardControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - dashboards.sort(idComparator); + expectedDashboards.sort(idComparator); loadedDashboards.sort(idComparator); - Assert.assertEquals(dashboards, loadedDashboards); + Assert.assertEquals(expectedDashboards, loadedDashboards); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 227d37f483..8d21057b8e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -18,8 +18,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -32,7 +30,6 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -46,7 +43,6 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -58,14 +54,16 @@ import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CA_ROOT_CERT_PEM; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.LINUX; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; -import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CA_ROOT_CERT_PEM; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS; @TestPropertySource(properties = { "device.connectivity.mqtts.pem_cert_file=/tmp/" + CA_ROOT_CERT_PEM @@ -241,12 +239,12 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); - assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " + + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", credentials.getCredentialsId())); JsonNode linuxCoapCommands = commands.get(COAP); @@ -256,10 +254,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); - assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --network=host" + - " thingsboard/coap-clients coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --network=host" + - " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + + " thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + + " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); } @Test @@ -285,15 +283,152 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); - assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + " -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); - assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " + + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); } + @Test + public void testFetchGatewayLaunchCommands() throws Exception { + String deviceName = "My device"; + Device device = new Device(); + device.setName(deviceName); + device.setType("default"); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("gateway", true); + device.setAdditionalInfo(additionalInfo); + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/gateway-launch/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + + String expectedContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", "");; + + JsonNode dockerMqttCommands = commands.get(MQTT); + assertThat(dockerMqttCommands.get(LINUX).asText()).isEqualTo(String.format("docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=1883 -e accessToken=%s --restart always thingsboard/tb-gateway", credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(WINDOWS).asText()).isEqualTo("docker run -it -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\logs:/thingsboard_gateway/logs -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\extensions:/thingsboard_gateway/extensions -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=1883 -e accessToken=" + credentials.getCredentialsId() + " --restart always thingsboard/tb-gateway"); + + JsonNode dockerMqttsCommands = commands.get(MQTTS); + assertThat(dockerMqttsCommands.get(LINUX).asText()).isEqualTo(String.format("docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=8883 -e accessToken=%s --restart always thingsboard/tb-gateway", credentials.getCredentialsId())); + assertThat(dockerMqttsCommands.get(WINDOWS).asText()).isEqualTo("docker run -it -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\logs:/thingsboard_gateway/logs -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\extensions:/thingsboard_gateway/extensions -v %HOMEDRIVE%%HOMEPATH%\\tb-gateway\\config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=8883 -e accessToken=" + credentials.getCredentialsId() + " --restart always thingsboard/tb-gateway"); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithIpV6LocalhostAddress() throws Exception { + loginSysAdmin(); + setConnectivityHost("::1"); + loginTenantAdmin(); + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + + assertThat(commands).hasSize(3); + JsonNode httpCommands = commands.get(HTTP); + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://[::1]:8080/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://[::1]/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h ::1 -p 1883 -t v1/devices/me/telemetry " + + "-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h ::1 -p 8883 " + + "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + + " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", + credentials.getCredentialsId())); + + JsonNode linuxCoapCommands = commands.get(COAP); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://[::1]:5683/api/v1/%s/telemetry " + + "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://[::1]:5684/api/v1/%s/telemetry" + + " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); + assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + + " thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway" + + " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithIpV6Address() throws Exception { + + loginSysAdmin(); + setConnectivityHost("1:1:1:1:1:1:1:1"); + loginTenantAdmin(); + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(3); + JsonNode httpCommands = commands.get(HTTP); + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://[1:1:1:1:1:1:1:1]:8080/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://[1:1:1:1:1:1:1:1]/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1 -p 1883 -t v1/devices/me/telemetry " + + "-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h 1:1:1:1:1:1:1:1 -p 8883 " + + "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1" + + " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h 1:1:1:1:1:1:1:1 -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", + credentials.getCredentialsId())); + + JsonNode linuxCoapCommands = commands.get(COAP); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry " + + "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry" + + " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); + assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it" + + " thingsboard/coap-clients coap-client -v 6 -m POST coap://[1:1:1:1:1:1:1:1]:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it" + + " thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://[1:1:1:1:1:1:1:1]:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + + } + @Test public void testFetchPublishTelemetryCommandsForDeviceWithMqttBasicCreds() throws Exception { Device device = new Device(); @@ -329,12 +464,12 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); - assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + " -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); - assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " + + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + - "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", + "mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); } @@ -518,47 +653,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { public void testFetchPublishTelemetryCommandsForDefaultDeviceIfHostIsNotLocalhost() throws Exception { loginSysAdmin(); - ObjectNode config = JacksonUtil.newObjectNode(); - - ObjectNode http = JacksonUtil.newObjectNode(); - http.put("enabled", true); - http.put("host", "test.domain"); - http.put("port", 8080); - config.set("http", http); - - ObjectNode https = JacksonUtil.newObjectNode(); - https.put("enabled", true); - https.put("host", "test.domain"); - https.put("port", 443); - config.set("https", https); - - ObjectNode mqtt = JacksonUtil.newObjectNode(); - mqtt.put("enabled", true); - mqtt.put("host", "test.domain"); - mqtt.put("port", 1883); - config.set("mqtt", mqtt); - - ObjectNode mqtts = JacksonUtil.newObjectNode(); - mqtts.put("enabled", true); - mqtts.put("host", "test.domain"); - mqtts.put("port", 8883); - config.set("mqtts", mqtts); - - ObjectNode coap = JacksonUtil.newObjectNode(); - coap.put("enabled", true); - coap.put("host", "test.domain"); - coap.put("port", 5683); - config.set("coap", coap); - - ObjectNode coaps = JacksonUtil.newObjectNode(); - coaps.put("enabled", true); - coaps.put("host", "test.domain"); - coaps.put("port", 5684); - config.set("coaps", coaps); - - AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); - adminSettings.setJsonValue(config); - doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); + setConnectivityHost("test.domain"); login("tenant2@thingsboard.org", "testPassword1"); @@ -612,4 +707,49 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it " + "thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); } + + + private void setConnectivityHost(String host) throws Exception { + ObjectNode config = JacksonUtil.newObjectNode(); + + ObjectNode http = JacksonUtil.newObjectNode(); + http.put("enabled", true); + http.put("host", host); + http.put("port", 8080); + config.set("http", http); + + ObjectNode https = JacksonUtil.newObjectNode(); + https.put("enabled", true); + https.put("host", host); + https.put("port", 443); + config.set("https", https); + + ObjectNode mqtt = JacksonUtil.newObjectNode(); + mqtt.put("enabled", true); + mqtt.put("host", host); + mqtt.put("port", 1883); + config.set("mqtt", mqtt); + + ObjectNode mqtts = JacksonUtil.newObjectNode(); + mqtts.put("enabled", true); + mqtts.put("host", host); + mqtts.put("port", 8883); + config.set("mqtts", mqtts); + + ObjectNode coap = JacksonUtil.newObjectNode(); + coap.put("enabled", true); + coap.put("host", host); + coap.put("port", 5683); + config.set("coap", coap); + + ObjectNode coaps = JacksonUtil.newObjectNode(); + coaps.put("enabled", true); + coaps.put("host", host); + coaps.put("port", 5684); + config.set("coaps", coaps); + + AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); + adminSettings.setJsonValue(config); + doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 778a11a552..7cafe2bbcb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -28,12 +28,14 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; @@ -51,7 +53,6 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -73,7 +74,9 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; +import org.thingsboard.server.service.state.DeviceStateService; import java.util.ArrayList; import java.util.List; @@ -82,6 +85,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -106,6 +110,9 @@ public class DeviceControllerTest extends AbstractControllerTest { @SpyBean private GatewayNotificationsService gatewayNotificationsService; + @SpyBean + private DeviceStateService deviceStateService; + @Autowired private DeviceDao deviceDao; @@ -1340,6 +1347,28 @@ public class DeviceControllerTest extends AbstractControllerTest { ActionType.ASSIGNED_TO_TENANT, savedDifferentTenant.getId().getId().toString(), savedDifferentTenant.getTitle()); testNotificationUpdateGatewayNever(); + ArgumentCaptor protoCaptor = ArgumentCaptor.forClass(TransportProtos.DeviceStateServiceMsgProto.class); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> { + Mockito.verify(deviceStateService, Mockito.atLeastOnce()).onQueueMsg(protoCaptor.capture(), any()); + return protoCaptor.getAllValues().stream().anyMatch(proto -> + proto.getTenantIdMSB() == savedTenant.getUuidId().getMostSignificantBits() && + proto.getTenantIdLSB() == savedTenant.getUuidId().getLeastSignificantBits() && + proto.getDeviceIdMSB() == savedDevice.getUuidId().getMostSignificantBits() && + proto.getDeviceIdLSB() == savedDevice.getUuidId().getLeastSignificantBits() && + proto.getDeleted()); + }); + + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> { + Mockito.verify(deviceStateService, Mockito.atLeastOnce()).onQueueMsg(protoCaptor.capture(), any()); + return protoCaptor.getAllValues().stream().anyMatch(proto -> + proto.getTenantIdMSB() == savedDifferentTenant.getUuidId().getMostSignificantBits() && + proto.getTenantIdLSB() == savedDifferentTenant.getUuidId().getLeastSignificantBits() && + proto.getDeviceIdMSB() == savedDevice.getUuidId().getMostSignificantBits() && + proto.getDeviceIdLSB() == savedDevice.getUuidId().getLeastSignificantBits() && + proto.getAdded()); + }); + login("tenant9@thingsboard.org", "testPassword1"); Device foundDevice1 = doGet("/api/device/" + assignedDevice.getId().getId(), Device.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index f0c47bb53a..530c8634d7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -23,6 +23,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.AbstractMessage; +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -37,14 +39,18 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -53,6 +59,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.edge.EdgeDao; @@ -78,10 +86,10 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.UUID; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; -import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -92,6 +100,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; }) @ContextConfiguration(classes = {EdgeControllerTest.Config.class}) @DaoSqlTest +@Slf4j public class EdgeControllerTest extends AbstractControllerTest { public static final String EDGE_HOST = "localhost"; @@ -121,7 +130,7 @@ public class EdgeControllerTest extends AbstractControllerTest { } @After - public void teardownEdgeTest() throws Exception { + public void teardownEdgeTest() { executor.shutdownNow(); } @@ -204,15 +213,13 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testFindEdgeTypesByTenantId() throws Exception { - List edges = new ArrayList<>(); - int cntEntity = 3; Mockito.reset(tbClusterService, auditLogService); for (int i = 0; i < cntEntity; i++) { Edge edge = constructEdge("My edge B" + i, "typeB"); - edges.add(doPost("/api/edge", edge, Edge.class)); + doPost("/api/edge", edge, Edge.class); } testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new Edge(), new Edge(), @@ -221,11 +228,11 @@ public class EdgeControllerTest extends AbstractControllerTest { for (int i = 0; i < 7; i++) { Edge edge = constructEdge("My edge C" + i, "typeC"); - edges.add(doPost("/api/edge", edge, Edge.class)); + doPost("/api/edge", edge, Edge.class); } for (int i = 0; i < 9; i++) { Edge edge = constructEdge("My edge A" + i, "typeA"); - edges.add(doPost("/api/edge", edge, Edge.class)); + doPost("/api/edge", edge, Edge.class); } List edgeTypes = doGetTyped("/api/edge/types", new TypeReference<>() { @@ -395,7 +402,7 @@ public class EdgeControllerTest extends AbstractControllerTest { List edges = new ArrayList<>(Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS)); List loadedEdges = new ArrayList<>(); PageLink pageLink = new PageLink(23); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/tenant/edges?", new TypeReference<>() { @@ -442,10 +449,10 @@ public class EdgeControllerTest extends AbstractControllerTest { List loadedEdgesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/tenant/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); loadedEdgesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -462,7 +469,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/tenant/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); loadedEdgesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -532,10 +539,10 @@ public class EdgeControllerTest extends AbstractControllerTest { List loadedEdgesType1 = new ArrayList<>(); PageLink pageLink = new PageLink(15); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type1); loadedEdgesType1.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -552,7 +559,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4); do { pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type2); loadedEdgesType2.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -572,7 +579,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -584,7 +591,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4); pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -613,15 +620,14 @@ public class EdgeControllerTest extends AbstractControllerTest { testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new Edge(), new Edge(), tenantId, customerId, tenantAdminUser.getId(), tenantAdminUser.getEmail(), - ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity * 2, - new String(), new String(), new String()); + ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, cntEntity * 2, "", "", ""); List loadedEdges = new ArrayList<>(); PageLink pageLink = new PageLink(23); PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); loadedEdges.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -679,7 +685,7 @@ public class EdgeControllerTest extends AbstractControllerTest { PageData pageData = null; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); loadedEdgesTitle1.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -696,7 +702,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); loadedEdgesTitle2.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -723,7 +729,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -735,7 +741,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title2); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", - new TypeReference>() { + new TypeReference<>() { }, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -784,10 +790,10 @@ public class EdgeControllerTest extends AbstractControllerTest { List loadedEdgesType1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); - PageData pageData = null; + PageData pageData; do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type1); loadedEdgesType1.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -804,7 +810,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title2); do { pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type2); loadedEdgesType2.addAll(pageData.getData()); if (pageData.hasNext()) { @@ -824,7 +830,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title1); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type1); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -836,7 +842,7 @@ public class EdgeControllerTest extends AbstractControllerTest { pageLink = new PageLink(4, 0, title2); pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", - new TypeReference>() { + new TypeReference<>() { }, pageLink, type2); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -863,10 +869,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Edge edge = doPost("/api/edge", constructEdge("Test Sync Edge", "test"), Edge.class); - // simulate edge activation - ObjectNode attributes = JacksonUtil.newObjectNode(); - attributes.put("active", true); - doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); + simulateEdgeActivation(edge); doPost("/api/edge/" + edge.getId().getId().toString() + "/device/" + savedDevice.getId().getId().toString(), Device.class); @@ -878,7 +881,7 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(24); edgeImitator.connect(); - assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); + waitForMessages(edgeImitator); verifyFetchersMsgs(edgeImitator); // verify queue msgs @@ -890,7 +893,7 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.expectMessageAmount(20); doPost("/api/edge/sync/" + edge.getId()); - assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); + waitForMessages(edgeImitator); verifyFetchersMsgs(edgeImitator); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); @@ -909,6 +912,35 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + private void simulateEdgeActivation(Edge edge) throws Exception { + ObjectNode attributes = JacksonUtil.newObjectNode(); + attributes.put("active", true); + doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); + Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); + if (activeAttrOpt.isEmpty()) { + return false; + } + Map activeAttr = activeAttrOpt.get(); + return "true".equals(activeAttr.get("value").toString()); + }); + } + + private void waitForMessages(EdgeImitator edgeImitator) throws Exception { + boolean success = edgeImitator.waitForMessages(); + if (!success) { + List downlinkMsgs = edgeImitator.getDownlinkMsgs(); + for (AbstractMessage downlinkMsg : downlinkMsgs) { + log.error("{}\n{}", downlinkMsg.getClass(), downlinkMsg); + } + Assert.fail("Await for messages was not successful!"); + } + } + private void verifyFetchersMsgs(EdgeImitator edgeImitator) { Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main")); Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain")); @@ -936,8 +968,9 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof QueueUpdateMsg) { QueueUpdateMsg queueUpdateMsg = (QueueUpdateMsg) message; - if (msgType.equals(queueUpdateMsg.getMsgType()) - && name.equals(queueUpdateMsg.getName())) { + Queue queue = JacksonUtil.fromStringIgnoreUnknownProperties(queueUpdateMsg.getEntity(), Queue.class); + Assert.assertNotNull(queue); + if (msgType.equals(queueUpdateMsg.getMsgType()) && name.equals(queue.getName())) { messages.remove(message); return true; } @@ -950,9 +983,11 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof RuleChainUpdateMsg) { RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) message; + RuleChain ruleChain = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainUpdateMsg.getEntity(), RuleChain.class); + Assert.assertNotNull(ruleChain); if (msgType.equals(ruleChainUpdateMsg.getMsgType()) - && name.equals(ruleChainUpdateMsg.getName()) - && ruleChainUpdateMsg.getRoot()) { + && name.equals(ruleChain.getName()) + && ruleChain.isRoot()) { messages.remove(message); return true; } @@ -965,7 +1000,9 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof AdminSettingsUpdateMsg) { AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message; - if (key.equals(adminSettingsUpdateMsg.getKey())) { + AdminSettings adminSettings = JacksonUtil.fromStringIgnoreUnknownProperties(adminSettingsUpdateMsg.getEntity(), AdminSettings.class); + Assert.assertNotNull(adminSettings); + if (key.equals(adminSettings.getKey())) { messages.remove(message); return true; } @@ -978,8 +1015,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof DeviceProfileUpdateMsg) { DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) message; + DeviceProfile deviceProfile = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfile); if (msgType.equals(deviceProfileUpdateMsg.getMsgType()) - && name.equals(deviceProfileUpdateMsg.getName())) { + && name.equals(deviceProfile.getName())) { messages.remove(message); return true; } @@ -992,8 +1031,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof DeviceUpdateMsg) { DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) message; + Device device = JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(device); if (msgType.equals(deviceUpdateMsg.getMsgType()) - && name.equals(deviceUpdateMsg.getName())) { + && name.equals(device.getName())) { messages.remove(message); return true; } @@ -1006,8 +1047,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof AssetProfileUpdateMsg) { AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) message; + AssetProfile assetProfile = JacksonUtil.fromStringIgnoreUnknownProperties(assetProfileUpdateMsg.getEntity(), AssetProfile.class); + Assert.assertNotNull(assetProfile); if (msgType.equals(assetProfileUpdateMsg.getMsgType()) - && name.equals(assetProfileUpdateMsg.getName())) { + && name.equals(assetProfile.getName())) { messages.remove(message); return true; } @@ -1020,8 +1063,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof AssetUpdateMsg) { AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) message; + Asset asset = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(asset); if (msgType.equals(assetUpdateMsg.getMsgType()) - && name.equals(assetUpdateMsg.getName())) { + && name.equals(asset.getName())) { messages.remove(message); return true; } @@ -1034,9 +1079,11 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof UserUpdateMsg) { UserUpdateMsg userUpdateMsg = (UserUpdateMsg) message; + User user = JacksonUtil.fromStringIgnoreUnknownProperties(userUpdateMsg.getEntity(), User.class); + Assert.assertNotNull(user); if (msgType.equals(userUpdateMsg.getMsgType()) - && email.equals(userUpdateMsg.getEmail()) - && authority.name().equals(userUpdateMsg.getAuthority())) { + && email.equals(user.getEmail()) + && authority.equals(user.getAuthority())) { messages.remove(message); return true; } @@ -1049,8 +1096,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof CustomerUpdateMsg) { CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) message; + Customer customer = JacksonUtil.fromStringIgnoreUnknownProperties(customerUpdateMsg.getEntity(), Customer.class); + Assert.assertNotNull(customer); if (msgType.equals(customerUpdateMsg.getMsgType()) - && title.equals(customerUpdateMsg.getTitle())) { + && title.equals(customer.getTitle())) { messages.remove(message); return true; } @@ -1063,9 +1112,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof TenantUpdateMsg) { TenantUpdateMsg tenantUpdateMsg = (TenantUpdateMsg) message; - TenantId tenantIdMsg = new TenantId(new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB())); + Tenant tenant = JacksonUtil.fromStringIgnoreUnknownProperties(tenantUpdateMsg.getEntity(), Tenant.class); + Assert.assertNotNull(tenant); if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantUpdateMsg.getMsgType()) - && tenantId1.equals(tenantIdMsg)) { + && tenantId1.equals(tenant.getId())) { messages.remove(message); return true; } @@ -1078,9 +1128,10 @@ public class EdgeControllerTest extends AbstractControllerTest { for (AbstractMessage message : messages) { if (message instanceof TenantProfileUpdateMsg) { TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) message; - TenantProfileId tenantProfileIdMsg = new TenantProfileId(new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB())); + TenantProfile tenantProfile = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfile); if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantProfileUpdateMsg.getMsgType()) - && tenantProfileId.equals(tenantProfileIdMsg)) { + && tenantProfileId.equals(tenantProfile.getId())) { messages.remove(message); return true; } diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index d020fef1cb..148c4afea9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -92,6 +92,8 @@ public class HomePageApiTest extends AbstractControllerTest { @MockBean private SmsService smsService; + private static final int DEFAULT_DASHBOARDS_COUNT = 1; + //For system administrator @Test public void testTenantsCountWsCmd() throws Exception { @@ -408,7 +410,7 @@ public class HomePageApiTest extends AbstractControllerTest { Assert.assertEquals(2, usageInfo.getUsers()); Assert.assertEquals(configuration.getMaxUsers(), usageInfo.getMaxUsers()); - Assert.assertEquals(0, usageInfo.getDashboards()); + Assert.assertEquals(DEFAULT_DASHBOARDS_COUNT, usageInfo.getDashboards()); Assert.assertEquals(configuration.getMaxDashboards(), usageInfo.getMaxDashboards()); Assert.assertEquals(0, usageInfo.getTransportMessages()); @@ -478,7 +480,8 @@ public class HomePageApiTest extends AbstractControllerTest { } usageInfo = doGet("/api/usage", UsageInfo.class); - Assert.assertEquals(dashboards.size(), usageInfo.getDashboards()); + int expectedDashboardsCount = dashboards.size() + DEFAULT_DASHBOARDS_COUNT; + Assert.assertEquals(expectedDashboardsCount, usageInfo.getDashboards()); } private Long getInitialEntityCount(EntityType entityType) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java new file mode 100644 index 0000000000..1fcf9abb57 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -0,0 +1,277 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.hash.Hashing; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.ImageDescriptor; +import org.thingsboard.server.common.data.ImageExportData; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.sql.resource.TbResourceRepository; + +import java.util.Base64; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class ImageControllerTest extends AbstractControllerTest { + + private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("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"); + private static final byte[] JPEG_IMAGE = Base64.getDecoder().decode("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"); + private static final byte[] SVG_IMAGE = Base64.getDecoder().decode("PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg=="); + + @Autowired + private TbResourceRepository resourceRepository; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + } + + @After + public void afterEach() { + resourceRepository.deleteAll(); + } + + @Test + public void testUploadPngImage() throws Exception { + String filename = "my_png_image.png"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + + assertThat(imageInfo.getTitle()).isEqualTo(filename); + assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE); + assertThat(imageInfo.getResourceKey()).isEqualTo(filename); + assertThat(imageInfo.getFileName()).isEqualTo(filename); + assertThat(imageInfo.getEtag()).isEqualTo(Hashing.sha256().hashBytes(PNG_IMAGE).toString()); + + checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class)); + + assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).containsExactly(PNG_IMAGE); + + assertThat(getImages(null, false, 10)).contains(imageInfo); + } + + @Test + public void testUploadJpegImage() throws Exception { + String filename = "my_jpeg_image.jpg"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + checkJpegImageDescriptor(imageDescriptor); + + assertThat(downloadImage("tenant", filename)).containsExactly(JPEG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize()); + } + + @Test + public void testUploadSvgImage() throws Exception { + String filename = "my_svg_image.svg"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE); + + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + checkSvgImageDescriptor(imageDescriptor); + + assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize()); + } + + @Test + public void testUploadImageWithSameFilename() throws Exception { + String filename = "my_jpeg_image.jpg"; + TbResourceInfo imageInfo1 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + assertThat(imageInfo1.getTitle()).isEqualTo(filename); + assertThat(imageInfo1.getFileName()).isEqualTo(filename); + assertThat(imageInfo1.getResourceKey()).isEqualTo(filename); + + TbResourceInfo imageInfo2 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + assertThat(imageInfo2.getTitle()).isEqualTo(filename); + assertThat(imageInfo2.getFileName()).isEqualTo(filename); + assertThat(imageInfo2.getResourceKey()).isEqualTo("my_jpeg_image_(1).jpg"); + + TbResourceInfo imageInfo3 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + assertThat(imageInfo3.getTitle()).isEqualTo(filename); + assertThat(imageInfo3.getFileName()).isEqualTo(filename); + assertThat(imageInfo3.getResourceKey()).isEqualTo("my_jpeg_image_(2).jpg"); + } + + @Test + public void testUpdateImage() throws Exception { + String filename = "my_png_image.png"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class)); + + String newFilename = "my_jpeg_image.png"; + imageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE); + + assertThat(imageInfo.getTitle()).isEqualTo(filename); + assertThat(imageInfo.getResourceKey()).isEqualTo(filename); + assertThat(imageInfo.getFileName()).isEqualTo(newFilename); + + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + checkJpegImageDescriptor(imageDescriptor); + + assertThat(downloadImage("tenant", filename)).containsExactly(JPEG_IMAGE); + assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize()); + } + + @Test + public void testUpdateImageInfo() throws Exception { + String filename = "my_png_image.png"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); + + assertThat(imageInfo.getTitle()).isEqualTo(filename); + assertThat(imageInfo.getResourceKey()).isEqualTo(filename); + assertThat(imageInfo.getFileName()).isEqualTo(filename); + + String newTitle = "My PNG image"; + imageInfo.setTitle(newTitle); + imageInfo.setDescriptor(JacksonUtil.newObjectNode()); + imageInfo = doPut("/api/images/tenant/" + filename + "/info", imageInfo, TbResourceInfo.class); + + assertThat(imageInfo.getTitle()).isEqualTo(newTitle); + assertThat(imageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor); + } + + @Test + public void testExportImportImage() throws Exception { + String filename = "my_png_image.png"; + uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + + ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class); + assertThat(exportData.getMediaType()).isEqualTo("image/png"); + assertThat(exportData.getFileName()).isEqualTo(filename); + assertThat(exportData.getTitle()).isEqualTo(filename); + assertThat(exportData.getResourceKey()).isEqualTo(filename); + assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE)); + + doDelete("/api/images/tenant/" + filename).andExpect(status().isOk()); + + TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class); + assertThat(importedImageInfo.getTitle()).isEqualTo(filename); + assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename); + assertThat(importedImageInfo.getFileName()).isEqualTo(filename); + checkPngImageDescriptor(importedImageInfo.getDescriptor(ImageDescriptor.class)); + assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE); + } + + @Test + public void testGetImages() throws Exception { + loginSysAdmin(); + String systemImageName = "my_system_png_image.png"; + TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE); + + loginTenantAdmin(); + String tenantImageName = "my_jpeg_image.jpg"; + TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE); + + List tenantImages = getImages(null, false, 10); + assertThat(tenantImages).containsOnly(tenantImage); + + List allImages = getImages(null, true, 10); + assertThat(allImages).containsOnly(tenantImage, systemImage); + + assertThat(getImages("png", true, 10)) + .containsOnly(systemImage); + assertThat(getImages("jpg", true, 10)) + .containsOnly(tenantImage); + } + + private void checkPngImageDescriptor(ImageDescriptor imageDescriptor) { + assertThat(imageDescriptor.getMediaType()).isEqualTo("image/png"); + assertThat(imageDescriptor.getWidth()).isEqualTo(200); + assertThat(imageDescriptor.getHeight()).isEqualTo(160); + assertThat(imageDescriptor.getSize()).isEqualTo(PNG_IMAGE.length); + assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(PNG_IMAGE).toString()); + + ImageDescriptor previewDescriptor = imageDescriptor.getPreviewDescriptor(); + assertThat(previewDescriptor.getMediaType()).isEqualTo("image/png"); + assertThat(previewDescriptor.getWidth()).isEqualTo(200); + assertThat(previewDescriptor.getHeight()).isEqualTo(160); + assertThat(previewDescriptor.getSize()).isEqualTo(PNG_IMAGE.length); + assertThat(previewDescriptor.getEtag()).isEqualTo(imageDescriptor.getEtag()); + } + + private void checkJpegImageDescriptor(ImageDescriptor imageDescriptor) { + assertThat(imageDescriptor.getMediaType()).isEqualTo("image/jpeg"); + assertThat(imageDescriptor.getWidth()).isEqualTo(225); + assertThat(imageDescriptor.getHeight()).isEqualTo(225); + assertThat(imageDescriptor.getSize()).isEqualTo(JPEG_IMAGE.length); + assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(JPEG_IMAGE).toString()); + + ImageDescriptor previewDescriptor = imageDescriptor.getPreviewDescriptor(); + assertThat(previewDescriptor.getMediaType()).isEqualTo("image/png"); + assertThat(previewDescriptor.getWidth()).isEqualTo(225); + assertThat(previewDescriptor.getHeight()).isEqualTo(225); + assertThat(previewDescriptor.getSize()).isEqualTo(49817); + assertThat(previewDescriptor.getEtag()).isEqualTo("d330b6c158cbf0c40fca0504d39add940693060302ab11b5c0ee79a41e5637d9"); + } + + private void checkSvgImageDescriptor(ImageDescriptor imageDescriptor) { + assertThat(imageDescriptor.getMediaType()).isEqualTo("image/svg+xml"); + assertThat(imageDescriptor.getWidth()).isEqualTo(150); + assertThat(imageDescriptor.getHeight()).isEqualTo(150); + assertThat(imageDescriptor.getSize()).isEqualTo(SVG_IMAGE.length); + assertThat(imageDescriptor.getEtag()).isEqualTo(Hashing.sha256().hashBytes(SVG_IMAGE).toString()); + + ImageDescriptor previewDescriptor = imageDescriptor.getPreviewDescriptor(); + assertThat(previewDescriptor.getMediaType()).isEqualTo("image/png"); + assertThat(previewDescriptor.getWidth()).isEqualTo(250); + assertThat(previewDescriptor.getHeight()).isEqualTo(250); + assertThat(previewDescriptor.getSize()).isEqualTo(13247); + assertThat(previewDescriptor.getEtag()).isEqualTo("f02db84f2c6ed7ea0606ae1145986a16a534cb0fd70436b3b5ca585569d48e46"); + } + + private List getImages(String searchText, boolean includeSystemImages, int limit) throws Exception { + PageData images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText)); + return images.getData(); + } + + private byte[] downloadImage(String type, String key) throws Exception { + return doGet("/api/images/" + type + "/" + key).andExpect(status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + } + + private byte[] downloadImagePreview(String type, String key) throws Exception { + return doGet("/api/images/" + type + "/" + key + "/preview").andExpect(status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + } + + private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception { + MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content); + MockMultipartHttpServletRequestBuilder request = MockMvcRequestBuilders.multipart(httpMethod, url); + request.file(file); + setJwtToken(request); + return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 8c52eb1303..20a9e7ced9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -35,6 +35,7 @@ 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.audit.ActionType; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -48,6 +49,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -101,7 +103,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -116,9 +118,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(resource.getTitle(), savedResource.getTitle()); Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getFileName()); Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey()); - Assert.assertEquals(resource.getData(), savedResource.getData()); + Assert.assertArrayEquals(resource.getData(), download(savedResource.getId())); savedResource.setTitle("My new resource"); + savedResource.setData(null); save(savedResource); @@ -136,7 +139,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle(StringUtils.randomAlphabetic(300)); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); Mockito.reset(tbClusterService, auditLogService); @@ -155,7 +158,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -184,13 +187,14 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); - TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); + TbResource foundResource = doGet("/api/resource/" + savedResource.getUuidId(), TbResource.class); Assert.assertNotNull(foundResource); - Assert.assertEquals(savedResource, foundResource); + Assert.assertEquals(savedResource.getId(), foundResource.getId()); + Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName()); } @Test @@ -199,7 +203,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -213,7 +217,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.DELETED, ActionType.DELETED, resourceIdStr); - doGet("/api/resource/" + savedResource.getId().getId().toString()) + doGet("/api/resource/" + savedResource.getUuidId()) .andExpect(status().isNotFound()) .andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr)))); } @@ -224,7 +228,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("My first resource"); resource.setFileName(DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -255,7 +259,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); @@ -292,7 +296,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("JKS Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); resources.add(new TbResourceInfo(save(resource))); } @@ -302,7 +306,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("LWM2M Resource" + i); resource.setResourceType(ResourceType.PKCS_12); resource.setFileName(i + DEFAULT_FILE_NAME_2); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); save(resource); } @@ -339,7 +343,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); resources.add(new TbResourceInfo(save(resource))); } List loadedResources = new ArrayList<>(); @@ -399,7 +403,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("JKS Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResourceInfo saved = new TbResourceInfo(save(resource)); jksResources.add(saved); } @@ -410,7 +414,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("LWM2M Resource" + i); resource.setResourceType(ResourceType.PKCS_12); resource.setFileName(i + DEFAULT_FILE_NAME_2); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource saved = save(resource); lwm2mesources.add(saved); } @@ -476,7 +480,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); expectedResources.add(new TbResourceInfo(save(resource))); } @@ -487,7 +491,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResourceInfo savedResource = new TbResourceInfo(save(resource)); systemResources.add(savedResource); if (i >= 73) { @@ -531,7 +535,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("Js resource"); resource.setFileName(JS_TEST_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -562,7 +566,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("Js resource"); resource.setFileName(JS_TEST_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -606,7 +610,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("Js resource"); resource.setFileName(JS_TEST_FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); TbResource savedResource = save(resource); @@ -616,6 +620,52 @@ public class TbResourceControllerTest extends AbstractControllerTest { .andExpect(status().isForbidden()); } + @Test + public void testUpdateResourceData_nonUpdatableResourceType() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.PKCS_12); + resource.setTitle("My resource"); + resource.setFileName("3.pks"); + resource.setEncodedData(TEST_DATA); + TbResource savedResource = save(resource); + resource.setEtag(savedResource.getEtag()); + + savedResource.setEncodedData(TEST_DATA); + doPost("/api/resource", savedResource) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("can't be updated"))); + + savedResource.setData(null); + savedResource.setTitle("Updated resource"); + savedResource = doPost("/api/resource", savedResource, TbResource.class); + assertThat(savedResource.getTitle()).isEqualTo("Updated resource"); + assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName()); + assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag()); + assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA); + } + + @Test + public void testUpdateResourceData_updatableResourceType() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName("module.js"); + resource.setEncodedData(TEST_DATA); + TbResource savedResource = save(resource); + resource.setEtag(savedResource.getEtag()); + + String newData = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3}); + savedResource.setEncodedData(newData); + savedResource.setFileName("new-module.js"); + savedResource.setTitle("Updated title"); + savedResource = save(savedResource); + + assertThat(savedResource.getTitle()).isEqualTo("Updated title"); + assertThat(savedResource.getFileName()).isEqualTo("new-module.js"); + assertThat(savedResource.getEtag()).isNotEqualTo(resource.getEtag()); + assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(newData); + } + @Test public void testGetLwm2mListObjectsPage() throws Exception { loginTenantAdmin(); @@ -623,7 +673,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>(){}); + doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() {}); Assert.assertNotNull(objects); Assert.assertEquals(resources.size(), objects.size()); @@ -637,13 +687,29 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>(){}); + doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>() {}); Assert.assertNotNull(objects); Assert.assertEquals(3, objects.size()); removeLoadResources(resources); } + private TbResource save(TbResource tbResource) throws Exception { + return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() { + }); + } + + private byte[] download(TbResourceId resourceId) throws Exception { + return doGet("/api/resource/" + resourceId + "/download") + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + } + + private String getImageLink(TbResourceInfo resourceInfo) { + return "/api/images/" + (resourceInfo.getTenantId().isSysTenantId() ? "system/" : "") + resourceInfo.getResourceKey(); + } + + private List loadLwm2mResources() throws Exception { var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303"); @@ -656,7 +722,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { TbResource resource = new TbResource(); resource.setResourceType(ResourceType.LWM2M_MODEL); resource.setFileName(fileName); - resource.setData(Base64.getEncoder().encodeToString(bytes)); + resource.setData(bytes); resources.add(save(resource)); } @@ -670,8 +736,4 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } - private TbResource save(TbResource tbResource) throws Exception { - return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() { - }); - } } diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java index 9cc9fd0596..e515250321 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java @@ -190,6 +190,32 @@ public class WidgetTypeControllerTest extends AbstractControllerTest { Collections.sort(loadedWidgetTypes, idComparator); Assert.assertEquals(widgetTypes, loadedWidgetTypes); + + loginCustomerUser(); + + List loadedWidgetTypesCustomer = doGetTyped("/api/widgetTypes?widgetsBundleId={widgetsBundleId}", + new TypeReference<>(){}, widgetsBundle.getId().getId().toString()); + Collections.sort(loadedWidgetTypesCustomer, idComparator); + Assert.assertEquals(widgetTypes, loadedWidgetTypesCustomer); + + List customerLoadedWidgetTypesDetails = doGetTyped("/api/widgetTypesDetails?widgetsBundleId={widgetsBundleId}", + new TypeReference<>(){}, widgetsBundle.getId().getId().toString()); + List widgetTypesFromDetailsListCustomer = customerLoadedWidgetTypesDetails.stream().map(WidgetType::new).collect(Collectors.toList()); + Collections.sort(widgetTypesFromDetailsListCustomer, idComparator); + Assert.assertEquals(widgetTypesFromDetailsListCustomer, loadedWidgetTypes); + + loginSysAdmin(); + + List sysAdminLoadedWidgetTypes = doGetTyped("/api/widgetTypes?widgetsBundleId={widgetsBundleId}", + new TypeReference<>(){}, widgetsBundle.getId().getId().toString()); + Collections.sort(sysAdminLoadedWidgetTypes, idComparator); + Assert.assertEquals(widgetTypes, sysAdminLoadedWidgetTypes); + + List sysAdminLoadedWidgetTypesDetails = doGetTyped("/api/widgetTypesDetails?widgetsBundleId={widgetsBundleId}", + new TypeReference<>(){}, widgetsBundle.getId().getId().toString()); + List widgetTypesFromDetailsListSysAdmin = sysAdminLoadedWidgetTypesDetails.stream().map(WidgetType::new).collect(Collectors.toList()); + Collections.sort(widgetTypesFromDetailsListSysAdmin, idComparator); + Assert.assertEquals(widgetTypesFromDetailsListSysAdmin, loadedWidgetTypes); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 2771a71eac..bf08967f16 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.asset.Asset; @@ -70,6 +72,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.controller.AbstractControllerTest; @@ -318,8 +321,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(tenantUpdateMsgOpt.isPresent()); TenantUpdateMsg tenantUpdateMsg = tenantUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); - UUID tenantUUID = new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB()); - Tenant tenant = doGet("/api/tenant/" + tenantUUID, Tenant.class); + Tenant tenantMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantUpdateMsg.getEntity(), Tenant.class); + Assert.assertNotNull(tenantMsg); + Tenant tenant = doGet("/api/tenant/" + tenantMsg.getUuidId(), Tenant.class); Assert.assertNotNull(tenant); testAutoGeneratedCodeByProtobuf(tenantUpdateMsg); } @@ -329,10 +333,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType()); - UUID tenantProfileUUID = new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()); + TenantProfile tenantProfile = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfile); Tenant tenant = doGet("/api/tenant/" + tenantId.getId(), Tenant.class); Assert.assertNotNull(tenant); - Assert.assertEquals(tenantProfileUUID, tenant.getTenantProfileId().getId()); + Assert.assertEquals(tenantProfile.getId(), tenant.getTenantProfileId()); testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg); } @@ -344,7 +349,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // thermostat msg from device fetcher Assert.assertEquals(expectedMsgCnt, deviceProfileUpdateMsgList.size()); Optional thermostatProfileUpdateMsgOpt = - deviceProfileUpdateMsgList.stream().filter(dfum -> THERMOSTAT_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny(); + deviceProfileUpdateMsgList.stream().filter(dfum -> { + DeviceProfile deviceProfile = JacksonUtil.fromStringIgnoreUnknownProperties(dfum.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + return THERMOSTAT_DEVICE_PROFILE_NAME.equals(deviceProfile.getName()); + }).findAny(); Assert.assertTrue(thermostatProfileUpdateMsgOpt.isPresent()); DeviceProfileUpdateMsg thermostatProfileUpdateMsg = thermostatProfileUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, thermostatProfileUpdateMsg.getMsgType()); @@ -419,8 +428,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); - UUID ruleChainUUID = new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()); - Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class); + Assert.assertNotNull(ruleChainMetaData); + Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId()); } private void validateAdminSettings(int expectedMsgCnt) { @@ -428,25 +438,29 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertEquals(expectedMsgCnt, adminSettingsUpdateMsgs.size()); for (AdminSettingsUpdateMsg adminSettingsUpdateMsg : adminSettingsUpdateMsgs) { - if (adminSettingsUpdateMsg.getKey().equals("general")) { - validateGeneralAdminSettings(adminSettingsUpdateMsg); + AdminSettings adminSettings = JacksonUtil.fromStringIgnoreUnknownProperties(adminSettingsUpdateMsg.getEntity(), AdminSettings.class); + Assert.assertNotNull(adminSettings); + if (adminSettings.getKey().equals("general")) { + validateGeneralAdminSettings(adminSettings); } - if (adminSettingsUpdateMsg.getKey().equals("mail")) { - validateMailAdminSettings(adminSettingsUpdateMsg); + if (adminSettings.getKey().equals("mail")) { + validateMailAdminSettings(adminSettings); } - if (adminSettingsUpdateMsg.getKey().equals("connectivity")) { - validateConnectivityAdminSettings(adminSettingsUpdateMsg); + if (adminSettings.getKey().equals("connectivity")) { + validateConnectivityAdminSettings(adminSettings); + } + if (adminSettings.getKey().equals("jwt")) { + validateJwtAdminSettings(adminSettings); } } } - private void validateGeneralAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { - JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); - Assert.assertNotNull(jsonNode.get("baseUrl")); + private void validateGeneralAdminSettings(AdminSettings adminSettings) { + Assert.assertNotNull(adminSettings.getJsonValue().get("baseUrl")); } - private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { - JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); + private void validateMailAdminSettings(AdminSettings adminSettings) { + JsonNode jsonNode = adminSettings.getJsonValue(); Assert.assertNotNull(jsonNode.get("mailFrom")); Assert.assertNotNull(jsonNode.get("smtpProtocol")); Assert.assertNotNull(jsonNode.get("smtpHost")); @@ -454,8 +468,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertNotNull(jsonNode.get("timeout")); } - private void validateConnectivityAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { - JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); + private void validateConnectivityAdminSettings(AdminSettings adminSettings) { + JsonNode jsonNode = adminSettings.getJsonValue(); Assert.assertNotNull(jsonNode.get("http")); Assert.assertNotNull(jsonNode.get("https")); Assert.assertNotNull(jsonNode.get("mqtt")); @@ -464,6 +478,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertNotNull(jsonNode.get("coaps")); } + private void validateJwtAdminSettings(AdminSettings adminSettings) { + JsonNode jsonNode = adminSettings.getJsonValue(); + Assert.assertNotNull(jsonNode.get("tokenExpirationTime")); + Assert.assertNotNull(jsonNode.get("refreshTokenExpTime")); + Assert.assertNotNull(jsonNode.get("tokenIssuer")); + Assert.assertNotNull(jsonNode.get("tokenSigningKey")); + } + private void validateAssetProfiles(int expectedMsgCnt) throws Exception { List assetProfileUpdateMsgs = edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class); Assert.assertEquals(expectedMsgCnt, assetProfileUpdateMsgs.size()); @@ -485,10 +507,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { UUID queueUUID = new UUID(queueUpdateMsg.getIdMSB(), queueUpdateMsg.getIdLSB()); Queue queue = doGet("/api/queues/" + queueUUID, Queue.class); Assert.assertNotNull(queue); - Assert.assertEquals(DataConstants.MAIN_QUEUE_NAME, queueUpdateMsg.getName()); - Assert.assertEquals(DataConstants.MAIN_QUEUE_TOPIC, queueUpdateMsg.getTopic()); - Assert.assertEquals(10, queueUpdateMsg.getPartitions()); - Assert.assertEquals(25, queueUpdateMsg.getPollInterval()); + Assert.assertEquals(DataConstants.MAIN_QUEUE_NAME, queue.getName()); + Assert.assertEquals(DataConstants.MAIN_QUEUE_TOPIC, queue.getTopic()); + Assert.assertEquals(10, queue.getPartitions()); + Assert.assertEquals(25, queue.getPollInterval()); testAutoGeneratedCodeByProtobuf(queueUpdateMsg); } @@ -500,7 +522,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { UUID userUUID = new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB()); User user = doGet("/api/user/" + userUUID, User.class); Assert.assertNotNull(user); - Assert.assertEquals("testtenant@thingsboard.org", userUpdateMsg.getEmail()); + Assert.assertEquals("testtenant@thingsboard.org", user.getEmail()); testAutoGeneratedCodeByProtobuf(userUpdateMsg); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java index 259572851c..804d2f6807 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java @@ -19,13 +19,14 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -46,16 +47,13 @@ public class AlarmEdgeTest extends AbstractEdgeTest { public void testSendAlarmToCloud() throws Exception { Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge(); - UUID alarmUUID = UUID.randomUUID(); + Alarm edgeAlarm = buildAlarmForUplinkMsg(device.getId()); + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AlarmUpdateMsg.Builder alarmUpdateMgBuilder = AlarmUpdateMsg.newBuilder(); - alarmUpdateMgBuilder.setIdMSB(alarmUUID.getMostSignificantBits()); - alarmUpdateMgBuilder.setIdLSB(alarmUUID.getLeastSignificantBits()); - alarmUpdateMgBuilder.setName("alarm from edge"); - alarmUpdateMgBuilder.setStatus(AlarmStatus.ACTIVE_UNACK.name()); - alarmUpdateMgBuilder.setSeverity(AlarmSeverity.CRITICAL.name()); - alarmUpdateMgBuilder.setOriginatorName(device.getName()); - alarmUpdateMgBuilder.setOriginatorType(EntityType.DEVICE.name()); + alarmUpdateMgBuilder.setIdMSB(edgeAlarm.getUuidId().getMostSignificantBits()); + alarmUpdateMgBuilder.setIdLSB(edgeAlarm.getUuidId().getLeastSignificantBits()); + alarmUpdateMgBuilder.setEntity(JacksonUtil.toString(edgeAlarm)); testAutoGeneratedCodeByProtobuf(alarmUpdateMgBuilder); uplinkMsgBuilder.addAlarmUpdateMsg(alarmUpdateMgBuilder.build()); @@ -72,7 +70,7 @@ public class AlarmEdgeTest extends AbstractEdgeTest { Optional foundAlarm = alarms.stream().filter(alarm -> alarm.getType().equals("alarm from edge")).findAny(); Assert.assertTrue(foundAlarm.isPresent()); AlarmInfo alarmInfo = foundAlarm.get(); - Assert.assertEquals(new AlarmId(alarmUUID), alarmInfo.getId()); + Assert.assertEquals(edgeAlarm.getId(), alarmInfo.getId()); Assert.assertEquals(device.getId(), alarmInfo.getOriginator()); Assert.assertEquals(AlarmStatus.ACTIVE_UNACK, alarmInfo.getStatus()); Assert.assertEquals(AlarmSeverity.CRITICAL, alarmInfo.getSeverity()); @@ -92,14 +90,10 @@ public class AlarmEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Alarm alarmMsg = JacksonUtil.fromStringIgnoreUnknownProperties(alarmUpdateMsg.getEntity(), Alarm.class); + Assert.assertNotNull(alarmMsg); + Assert.assertEquals(savedAlarm, alarmMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - Assert.assertEquals(savedAlarm.getUuidId().getMostSignificantBits(), alarmUpdateMsg.getIdMSB()); - Assert.assertEquals(savedAlarm.getUuidId().getLeastSignificantBits(), alarmUpdateMsg.getIdLSB()); - Assert.assertEquals(savedAlarm.getType(), alarmUpdateMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmUpdateMsg.getName()); - Assert.assertEquals(device.getName(), alarmUpdateMsg.getOriginatorName()); - Assert.assertEquals(savedAlarm.getStatus().name(), alarmUpdateMsg.getStatus()); - Assert.assertEquals(savedAlarm.getSeverity().name(), alarmUpdateMsg.getSeverity()); // ack alarm edgeImitator.expectMessageAmount(1); @@ -109,10 +103,11 @@ public class AlarmEdgeTest extends AbstractEdgeTest { Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ALARM_ACK_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - Assert.assertEquals(savedAlarm.getType(), alarmUpdateMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmUpdateMsg.getName()); - Assert.assertEquals(device.getName(), alarmUpdateMsg.getOriginatorName()); - Assert.assertEquals(AlarmStatus.ACTIVE_ACK.name(), alarmUpdateMsg.getStatus()); + alarmMsg = JacksonUtil.fromStringIgnoreUnknownProperties(alarmUpdateMsg.getEntity(), Alarm.class); + Assert.assertNotNull(alarmMsg); + Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); + Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); + Assert.assertEquals(AlarmStatus.ACTIVE_ACK, alarmMsg.getStatus()); // clear alarm edgeImitator.expectMessageAmount(1); @@ -122,10 +117,11 @@ public class AlarmEdgeTest extends AbstractEdgeTest { Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - Assert.assertEquals(savedAlarm.getType(), alarmUpdateMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmUpdateMsg.getName()); - Assert.assertEquals(device.getName(), alarmUpdateMsg.getOriginatorName()); - Assert.assertEquals(AlarmStatus.CLEARED_ACK.name(), alarmUpdateMsg.getStatus()); + alarmMsg = JacksonUtil.fromStringIgnoreUnknownProperties(alarmUpdateMsg.getEntity(), Alarm.class); + Assert.assertNotNull(alarmMsg); + Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); + Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); + Assert.assertEquals(AlarmStatus.CLEARED_ACK, alarmMsg.getStatus()); // delete alarm edgeImitator.expectMessageAmount(1); @@ -136,10 +132,20 @@ public class AlarmEdgeTest extends AbstractEdgeTest { Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - Assert.assertEquals(savedAlarm.getType(), alarmUpdateMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmUpdateMsg.getName()); - Assert.assertEquals(device.getName(), alarmUpdateMsg.getOriginatorName()); - Assert.assertEquals(AlarmStatus.CLEARED_ACK.name(), alarmUpdateMsg.getStatus()); + alarmMsg = JacksonUtil.fromStringIgnoreUnknownProperties(alarmUpdateMsg.getEntity(), Alarm.class); + Assert.assertNotNull(alarmMsg); + Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); + Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); + Assert.assertEquals(AlarmStatus.CLEARED_ACK, alarmMsg.getStatus()); } + private Alarm buildAlarmForUplinkMsg(DeviceId deviceId) { + Alarm alarm = new Alarm(); + alarm.setId(new AlarmId(UUID.randomUUID())); + alarm.setTenantId(tenantId); + alarm.setType("alarm from edge"); + alarm.setOriginator(deviceId); + alarm.setSeverity(AlarmSeverity.CRITICAL); + return alarm; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index 0eb19a17fb..1e35b6266b 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -15,17 +15,17 @@ */ package org.thingsboard.server.edge; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; @@ -57,11 +57,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); Assert.assertTrue(assetUpdateMsgOpt.isPresent()); AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.get(); + Asset assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB()); - Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); - Assert.assertEquals(savedAsset.getName(), assetUpdateMsg.getName()); - Assert.assertEquals(savedAsset.getType(), assetUpdateMsg.getType()); + Assert.assertEquals(savedAsset, assetMsg); Optional assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); @@ -77,8 +76,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); assetUpdateMsg = (AssetUpdateMsg) latestMessage; + assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); + Assert.assertEquals(savedAsset.getName(), assetMsg.getName()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedAsset.getName(), assetUpdateMsg.getName()); // unassign asset from edge edgeImitator.expectMessageAmount(1); @@ -107,11 +108,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); Assert.assertTrue(assetUpdateMsgOpt.isPresent()); assetUpdateMsg = assetUpdateMsgOpt.get(); + assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB()); - Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); - Assert.assertEquals(savedAsset.getName(), assetUpdateMsg.getName()); - Assert.assertEquals(savedAsset.getType(), assetUpdateMsg.getType()); + Assert.assertEquals(savedAsset, assetMsg); assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); assetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); @@ -135,9 +135,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); assetUpdateMsg = (AssetUpdateMsg) latestMessage; + assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), assetUpdateMsg.getCustomerIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getCustomerIdLSB()); + Assert.assertEquals(savedCustomer.getId(), assetMsg.getCustomerId()); // unassign asset #2 from customer edgeImitator.expectMessageAmount(1); @@ -146,10 +147,10 @@ public class AssetEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); assetUpdateMsg = (AssetUpdateMsg) latestMessage; + assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); - Assert.assertEquals( - new CustomerId(EntityId.NULL_UUID), - new CustomerId(new UUID(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()))); + Assert.assertEquals(new CustomerId(EntityId.NULL_UUID), assetMsg.getCustomerId()); // delete asset #2 - messages expected edgeImitator.expectMessageAmount(1); @@ -166,14 +167,13 @@ public class AssetEdgeTest extends AbstractEdgeTest { @Test public void testSendAssetToCloud() throws Exception { - UUID uuid = Uuids.timeBased(); + Asset asset = buildAssetForUplinkMsg("Asset Edge 2"); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); - assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - assetUpdateMsgBuilder.setName("Asset Edge 2"); - assetUpdateMsgBuilder.setType("default"); + assetUpdateMsgBuilder.setIdMSB(asset.getUuidId().getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(asset.getUuidId().getLeastSignificantBits()); + assetUpdateMsgBuilder.setEntity(JacksonUtil.toString(asset)); assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); @@ -188,9 +188,9 @@ public class AssetEdgeTest extends AbstractEdgeTest { UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); Assert.assertTrue(latestResponseMsg.getSuccess()); - Asset asset = doGet("/api/asset/" + uuid, Asset.class); - Assert.assertNotNull(asset); - Assert.assertEquals("Asset Edge 2", asset.getName()); + Asset foundAsset = doGet("/api/asset/" + asset.getUuidId(), Asset.class); + Assert.assertNotNull(foundAsset); + Assert.assertEquals("Asset Edge 2", foundAsset.getName()); } @Test @@ -198,14 +198,11 @@ public class AssetEdgeTest extends AbstractEdgeTest { String assetOnCloudName = StringUtils.randomAlphanumeric(15); Asset assetOnCloud = saveAsset(assetOnCloudName); - UUID uuid = Uuids.timeBased(); + Asset assetOnEdge = buildAssetForUplinkMsg(assetOnCloudName); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); - assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - assetUpdateMsgBuilder.setName(assetOnCloudName); - assetUpdateMsgBuilder.setType("default"); + assetUpdateMsgBuilder.setEntity(JacksonUtil.toString(assetOnEdge)); assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); @@ -223,7 +220,9 @@ public class AssetEdgeTest extends AbstractEdgeTest { Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); Assert.assertTrue(assetUpdateMsgOpt.isPresent()); AssetUpdateMsg latestAssetUpdateMsg = assetUpdateMsgOpt.get(); - Assert.assertNotEquals(assetOnCloudName, latestAssetUpdateMsg.getName()); + Asset assetMsg = JacksonUtil.fromStringIgnoreUnknownProperties(latestAssetUpdateMsg.getEntity(), Asset.class); + Assert.assertNotNull(assetMsg); + Assert.assertNotEquals(assetOnCloudName, assetMsg.getName()); UUID newAssetId = new UUID(latestAssetUpdateMsg.getIdMSB(), latestAssetUpdateMsg.getIdLSB()); @@ -275,4 +274,12 @@ public class AssetEdgeTest extends AbstractEdgeTest { return savedAsset; } + private Asset buildAssetForUplinkMsg(String name) { + Asset asset = new Asset(); + asset.setId(new AssetId(UUID.randomUUID())); + asset.setTenantId(tenantId); + asset.setName(name); + asset.setType("test"); + return asset; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index ebf0af1938..7ffff29ed7 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.edge; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -30,7 +30,6 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.UUID; @@ -52,12 +51,12 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + AssetProfile assetProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetProfileUpdateMsg.getEntity(), AssetProfile.class); + Assert.assertNotNull(assetProfileMsg); + Assert.assertEquals(assetProfile, assetProfileMsg); + Assert.assertEquals("Building", assetProfileMsg.getName()); + Assert.assertEquals(buildingsRuleChainId, assetProfileMsg.getDefaultEdgeRuleChainId()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); - Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); - Assert.assertEquals("Building", assetProfileUpdateMsg.getName()); - Assert.assertEquals(buildingsRuleChainId.getId().getMostSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdMSB()); - Assert.assertEquals(buildingsRuleChainId.getId().getLeastSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); // update asset profile assetProfile.setImage("IMAGE"); @@ -67,8 +66,10 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + assetProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(assetProfileUpdateMsg.getEntity(), AssetProfile.class); + Assert.assertNotNull(assetProfileMsg); + Assert.assertEquals("IMAGE", assetProfileMsg.getImage()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); - Assert.assertEquals(ByteString.copyFrom("IMAGE".getBytes(StandardCharsets.UTF_8)), assetProfileUpdateMsg.getImage()); // delete profile edgeImitator.expectMessageAmount(1); @@ -90,18 +91,15 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { RuleChainId edgeRuleChainId = createEdgeRuleChainAndAssignToEdge("Asset Profile Rule Chain"); DashboardId dashboardId = createDashboardAndAssignToEdge("Asset Profile Dashboard"); - UUID uuid = Uuids.timeBased(); + AssetProfile assetProfileOnEdge = buildAssetProfileForUplinkMsg("Asset Profile On Edge"); + assetProfileOnEdge.setDefaultRuleChainId(edgeRuleChainId); + assetProfileOnEdge.setDefaultDashboardId(dashboardId); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder(); - assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - assetProfileUpdateMsgBuilder.setName("Asset Profile On Edge"); - assetProfileUpdateMsgBuilder.setDefault(false); - assetProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(edgeRuleChainId.getId().getMostSignificantBits()); - assetProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(edgeRuleChainId.getId().getLeastSignificantBits()); - assetProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); - assetProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setIdMSB(assetProfileOnEdge.getUuidId().getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setIdLSB(assetProfileOnEdge.getUuidId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setEntity(JacksonUtil.toString(assetProfileOnEdge)); assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(assetProfileUpdateMsgBuilder); uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build()); @@ -116,7 +114,7 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); Assert.assertTrue(latestResponseMsg.getSuccess()); - AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class); + AssetProfile assetProfile = doGet("/api/assetProfile/" + assetProfileOnEdge.getUuidId(), AssetProfile.class); Assert.assertNotNull(assetProfile); Assert.assertEquals("Asset Profile On Edge", assetProfile.getName()); Assert.assertEquals(dashboardId, assetProfile.getDefaultDashboardId()); @@ -149,13 +147,11 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { assetProfileOnCloud = doPost("/api/assetProfile", assetProfileOnCloud, AssetProfile.class); Assert.assertTrue(edgeImitator.waitForMessages()); - UUID uuid = Uuids.timeBased(); + AssetProfile assetProfileOnEdge = buildAssetProfileForUplinkMsg(assetProfileOnCloudName); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder(); - assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - assetProfileUpdateMsgBuilder.setName(assetProfileOnCloudName); + assetProfileUpdateMsgBuilder.setEntity(JacksonUtil.toString(assetProfileOnEdge)); assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build()); @@ -172,12 +168,23 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { Optional assetProfileUpdateMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); Assert.assertTrue(assetProfileUpdateMsgOpt.isPresent()); AssetProfileUpdateMsg latestAssetProfileUpdateMsg = assetProfileUpdateMsgOpt.get(); - Assert.assertNotEquals(assetProfileOnCloudName, latestAssetProfileUpdateMsg.getName()); + AssetProfile assetProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(latestAssetProfileUpdateMsg.getEntity(), AssetProfile.class); + Assert.assertNotNull(assetProfileMsg); + Assert.assertNotEquals(assetProfileOnCloudName, assetProfileMsg.getName()); - Assert.assertNotEquals(assetProfileOnCloud.getUuidId(), uuid); + Assert.assertNotEquals(assetProfileOnCloud.getUuidId(), assetProfileOnEdge.getUuidId()); - AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class); + AssetProfile assetProfile = doGet("/api/assetProfile/" + assetProfileMsg.getUuidId(), AssetProfile.class); Assert.assertNotNull(assetProfile); Assert.assertNotEquals(assetProfileOnCloudName, assetProfile.getName()); } + + private AssetProfile buildAssetProfileForUplinkMsg(String name) { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setId(new AssetProfileId(UUID.randomUUID())); + assetProfile.setTenantId(tenantId); + assetProfile.setName(name); + assetProfile.setDefault(false); + return assetProfile; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java index 6cc15837d9..125aa41e18 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CustomerEdgeTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -57,10 +58,9 @@ public class CustomerEdgeTest extends AbstractEdgeTest { Optional customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); Assert.assertTrue(customerUpdateOpt.isPresent()); CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); + Customer customerMsg = JacksonUtil.fromStringIgnoreUnknownProperties(customerUpdateMsg.getEntity(), Customer.class); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB()); - Assert.assertEquals(savedCustomer.getTitle(), customerUpdateMsg.getTitle()); + Assert.assertEquals(savedCustomer, customerMsg); testAutoGeneratedCodeByProtobuf(customerUpdateMsg); // update customer @@ -71,10 +71,9 @@ public class CustomerEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + customerMsg = JacksonUtil.fromStringIgnoreUnknownProperties(customerUpdateMsg.getEntity(), Customer.class); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB()); - Assert.assertEquals(savedCustomer.getTitle(), customerUpdateMsg.getTitle()); + Assert.assertEquals(savedCustomer, customerMsg); // delete customer edgeImitator.expectMessageAmount(2); diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 6180ced49c..fc342bf682 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.edge; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; @@ -27,6 +26,7 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -36,7 +36,6 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -63,13 +62,12 @@ public class DashboardEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Dashboard dashboardMsg = JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); + Assert.assertNotNull(dashboardMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); - Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB()); - Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); - Assert.assertEquals(savedDashboard.getTitle(), dashboardUpdateMsg.getTitle()); - Assert.assertTrue(dashboardUpdateMsg.getMobileHide()); - Assert.assertEquals(IMAGE, dashboardUpdateMsg.getImage()); - Assert.assertEquals(MOBILE_ORDER, dashboardUpdateMsg.getMobileOrder()); + Assert.assertEquals(savedDashboard, dashboardMsg); + Assert.assertEquals(IMAGE, dashboardMsg.getImage()); + Assert.assertEquals(MOBILE_ORDER, dashboardMsg.getMobileOrder().intValue()); testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); // update dashboard @@ -80,8 +78,10 @@ public class DashboardEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + dashboardMsg = JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); + Assert.assertNotNull(dashboardMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); - Assert.assertEquals(savedDashboard.getTitle(), dashboardUpdateMsg.getTitle()); + Assert.assertEquals(savedDashboard.getTitle(), dashboardMsg.getTitle()); // unassign dashboard from edge edgeImitator.expectMessageAmount(1); @@ -112,10 +112,12 @@ public class DashboardEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + dashboardMsg = JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); + Assert.assertNotNull(dashboardMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), dashboardUpdateMsg.getIdMSB()); Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); - Assert.assertEquals(savedDashboard.getTitle(), dashboardUpdateMsg.getTitle()); + Assert.assertEquals(savedDashboard.getTitle(), dashboardMsg.getTitle()); // assign dashboard #2 to customer Customer customer = new Customer(); @@ -133,12 +135,12 @@ public class DashboardEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + dashboardMsg = JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); + Assert.assertNotNull(dashboardMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); - Set assignedCustomers = - JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {}); - Assert.assertNotNull(assignedCustomers); - Assert.assertFalse(assignedCustomers.isEmpty()); - Assert.assertTrue(assignedCustomers.contains(new ShortCustomerInfo(savedCustomer.getId(), customer.getTitle(), customer.isPublic()))); + Assert.assertNotNull(dashboardMsg.getAssignedCustomers()); + Assert.assertFalse(dashboardMsg.getAssignedCustomers().isEmpty()); + Assert.assertTrue(dashboardMsg.getAssignedCustomers().contains(new ShortCustomerInfo(savedCustomer.getId(), customer.getTitle(), customer.isPublic()))); // unassign dashboard #2 from customer edgeImitator.expectMessageAmount(1); @@ -148,11 +150,11 @@ public class DashboardEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + dashboardMsg = JacksonUtil.fromStringIgnoreUnknownProperties(dashboardUpdateMsg.getEntity(), Dashboard.class); + Assert.assertNotNull(dashboardMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); - assignedCustomers = - JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {}); - Assert.assertNotNull(assignedCustomers); - Assert.assertTrue(assignedCustomers.isEmpty()); + Assert.assertNotNull(dashboardMsg.getAssignedCustomers()); + Assert.assertTrue(dashboardMsg.getAssignedCustomers().isEmpty()); // delete dashboard #2 - messages expected edgeImitator.expectMessageAmount(1); @@ -169,14 +171,13 @@ public class DashboardEdgeTest extends AbstractEdgeTest { @Test public void testSendDashboardToCloud() throws Exception { - UUID uuid = Uuids.timeBased(); + Dashboard dashboard = buildDashboardForUplinkMsg(); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); - dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard"); - dashboardUpdateMsgBuilder.setConfiguration(""); + dashboardUpdateMsgBuilder.setIdMSB(dashboard.getUuidId().getMostSignificantBits()); + dashboardUpdateMsgBuilder.setIdLSB(dashboard.getUuidId().getLeastSignificantBits()); + dashboardUpdateMsgBuilder.setEntity(JacksonUtil.toString(dashboard)); dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder); uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); @@ -188,9 +189,9 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForResponses()); - Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); - Assert.assertNotNull(dashboard); - Assert.assertEquals("Edge Test Dashboard", dashboard.getName()); + Dashboard foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class); + Assert.assertNotNull(foundDashboard); + Assert.assertEquals("Edge Test Dashboard", foundDashboard.getName()); } @Test @@ -237,4 +238,12 @@ public class DashboardEdgeTest extends AbstractEdgeTest { return savedDashboard; } + private Dashboard buildDashboardForUplinkMsg() { + Dashboard dashboard = new Dashboard(); + dashboard.setId(new DashboardId(UUID.randomUUID())); + dashboard.setTenantId(tenantId); + dashboard.setTitle("Edge Test Dashboard"); + return dashboard; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index e6fee370f1..56b8897ed3 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -51,7 +52,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -117,11 +118,13 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); deviceUpdateMsg = deviceUpdateMsgOpt.get(); + Device deviceFromMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); - Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); - Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); - Assert.assertEquals(savedDevice.getName(), deviceUpdateMsg.getName()); - Assert.assertEquals(savedDevice.getType(), deviceUpdateMsg.getType()); + Assert.assertEquals(savedDevice, deviceFromMsg); + Assert.assertEquals(savedDevice.getId(), deviceFromMsg.getId()); + Assert.assertEquals(savedDevice.getName(), deviceFromMsg.getName()); + Assert.assertEquals(savedDevice.getType(), deviceFromMsg.getType()); Optional deviceProfileUpdateMsgOpt = edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class); Assert.assertTrue(deviceProfileUpdateMsgOpt.isPresent()); @@ -146,9 +149,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceFromMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getCustomerIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getCustomerIdLSB()); + Assert.assertEquals(savedCustomer.getId(), deviceFromMsg.getCustomerId()); // unassign device #2 from customer edgeImitator.expectMessageAmount(1); @@ -157,10 +161,12 @@ public class DeviceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceFromMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals( new CustomerId(EntityId.NULL_UUID), - new CustomerId(new UUID(deviceUpdateMsg.getCustomerIdMSB(), deviceUpdateMsg.getCustomerIdLSB()))); + new CustomerId(new UUID(deviceFromMsg.getCustomerId().getId().getMostSignificantBits(), deviceFromMsg.getCustomerId().getId().getLeastSignificantBits()))); // delete device #2 - messages expected edgeImitator.expectMessageAmount(1); @@ -196,9 +202,8 @@ public class DeviceEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), deviceCredentialsUpdateMsg.getCredentialsType()); - Assert.assertEquals(deviceCredentials.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsId()); - Assert.assertFalse(deviceCredentialsUpdateMsg.hasCredentialsValue()); + DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class); + Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); // update device credentials - X509_CERTIFICATE edgeImitator.expectMessageAmount(1); @@ -211,10 +216,12 @@ public class DeviceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), deviceCredentialsUpdateMsg.getCredentialsType()); - Assert.assertFalse(deviceCredentialsUpdateMsg.getCredentialsId().isEmpty()); - Assert.assertTrue(deviceCredentialsUpdateMsg.hasCredentialsValue()); - Assert.assertEquals(deviceCredentials.getCredentialsValue(), deviceCredentialsUpdateMsg.getCredentialsValue()); + deviceCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class); + Assert.assertNotNull(deviceCredentialsMsg); + Assert.assertEquals(deviceCredentials.getCredentialsType(), deviceCredentialsMsg.getCredentialsType()); + Assert.assertFalse(deviceCredentialsMsg.getCredentialsId().isEmpty()); + Assert.assertNotNull(deviceCredentialsMsg.getCredentialsValue()); + Assert.assertEquals(deviceCredentials.getCredentialsValue(), deviceCredentialsMsg.getCredentialsValue()); } private void verifyUpdateFirmwareIdSoftwareIdAndDeviceData(Device savedDevice) throws InterruptedException { @@ -244,19 +251,13 @@ public class DeviceEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Device deviceMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(deviceMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); - Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); - Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); - Assert.assertEquals(savedDevice.getName(), deviceUpdateMsg.getName()); - Assert.assertEquals(savedDevice.getType(), deviceUpdateMsg.getType()); - Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getFirmwareIdMSB()); - Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getFirmwareIdLSB()); - Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getSoftwareIdMSB()); - Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getSoftwareIdLSB()); - Optional deviceDataOpt = - dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); - Assert.assertTrue(deviceDataOpt.isPresent()); - deviceData = deviceDataOpt.get(); + Assert.assertEquals(savedDevice, deviceMsg); + Assert.assertEquals(firmwareOtaPackageInfo.getId(), deviceMsg.getFirmwareId()); + Assert.assertEquals(softwareOtaPackageInfo.getId(), deviceMsg.getSoftwareId()); + deviceData = deviceMsg.getDeviceData(); Assert.assertTrue(deviceData.getTransportConfiguration() instanceof MqttDeviceTransportConfiguration); MqttDeviceTransportConfiguration mqttDeviceTransportConfiguration = (MqttDeviceTransportConfiguration) deviceData.getTransportConfiguration(); @@ -276,14 +277,13 @@ public class DeviceEdgeTest extends AbstractEdgeTest { loginTenantAdmin(); - UUID uuid = Uuids.timeBased(); + Device device = buildDeviceForUplinkMsg("Edge Device", DEFAULT_DEVICE_TYPE); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); - deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - deviceUpdateMsgBuilder.setName("Edge Device"); - deviceUpdateMsgBuilder.setType(DEFAULT_DEVICE_TYPE); + deviceUpdateMsgBuilder.setIdMSB(device.getUuidId().getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(device.getUuidId().getLeastSignificantBits()); + deviceUpdateMsgBuilder.setEntity(JacksonUtil.toString(device)); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); @@ -327,12 +327,11 @@ public class DeviceEdgeTest extends AbstractEdgeTest { public void testSendDeviceCredentialsUpdateToCloud() throws Exception { Device device = findDeviceByName("Edge Device 1"); + DeviceCredentials deviceCredentials = buildDeviceCredentialsForUplinkMsg(device.getId()); + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceCredentialsUpdateMsg.Builder deviceCredentialsUpdateMsgBuilder = DeviceCredentialsUpdateMsg.newBuilder(); - deviceCredentialsUpdateMsgBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); - deviceCredentialsUpdateMsgBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); - deviceCredentialsUpdateMsgBuilder.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN.name()); - deviceCredentialsUpdateMsgBuilder.setCredentialsId("NEW_TOKEN"); + deviceCredentialsUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceCredentials)); testAutoGeneratedCodeByProtobuf(deviceCredentialsUpdateMsgBuilder); uplinkMsgBuilder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsgBuilder.build()); @@ -367,10 +366,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdMSB(), device.getUuidId().getMostSignificantBits()); - Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdLSB(), device.getUuidId().getLeastSignificantBits()); - Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsType(), deviceCredentials.getCredentialsType().name()); - Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentials.getCredentialsId()); + DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class); + Assert.assertNotNull(deviceCredentialsMsg); + Assert.assertEquals(device.getId(), deviceCredentialsMsg.getDeviceId()); + Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); } @Test @@ -485,14 +484,13 @@ public class DeviceEdgeTest extends AbstractEdgeTest { String deviceOnCloudName = StringUtils.randomAlphanumeric(15); Device deviceOnCloud = saveDevice(deviceOnCloudName, DEFAULT_DEVICE_TYPE); - UUID uuid = Uuids.timeBased(); + Device deviceMsg = buildDeviceForUplinkMsg(deviceOnCloudName, "test"); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); - deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - deviceUpdateMsgBuilder.setName(deviceOnCloudName); - deviceUpdateMsgBuilder.setType("default"); + deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(deviceMsg.getUuidId().getLeastSignificantBits()); + deviceUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceMsg)); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); @@ -509,7 +507,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); DeviceUpdateMsg latestDeviceUpdateMsg = deviceUpdateMsgOpt.get(); - Assert.assertNotEquals(deviceOnCloudName, latestDeviceUpdateMsg.getName()); + Device deviceLatestMsg = JacksonUtil.fromStringIgnoreUnknownProperties(latestDeviceUpdateMsg.getEntity(), Device.class); + Assert.assertNotNull(deviceLatestMsg); + Assert.assertNotEquals(deviceOnCloudName, deviceLatestMsg.getName()); UUID newDeviceId = new UUID(latestDeviceUpdateMsg.getIdMSB(), latestDeviceUpdateMsg.getIdLSB()); @@ -522,8 +522,8 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsRequestMsg.class); Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = deviceCredentialsUpdateMsgOpt.get(); - Assert.assertEquals(uuid.getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(uuid.getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); @@ -534,14 +534,13 @@ public class DeviceEdgeTest extends AbstractEdgeTest { @Test public void testSendDeviceToCloud() throws Exception { - UUID uuid = Uuids.timeBased(); + Device deviceMsg = buildDeviceForUplinkMsg("Edge Device 2", "test"); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); - deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - deviceUpdateMsgBuilder.setName("Edge Device 2"); - deviceUpdateMsgBuilder.setType("default"); + deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(deviceMsg.getUuidId().getLeastSignificantBits()); + deviceUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceMsg)); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); @@ -553,11 +552,11 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForMessages()); - Optional deviceCredentialsRequestMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsRequestMsg.class); - Assert.assertTrue(deviceCredentialsRequestMsgOpt.isPresent()); - DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = deviceCredentialsRequestMsgOpt.get(); - Assert.assertEquals(uuid.getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(uuid.getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg); + DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage; + Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); + Assert.assertEquals(deviceMsg.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); UUID newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); @@ -769,4 +768,22 @@ public class DeviceEdgeTest extends AbstractEdgeTest { } } } + + private Device buildDeviceForUplinkMsg(String name, String type) { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + device.setTenantId(tenantId); + device.setType(type); + device.setName(name); + return device; + } + + + private DeviceCredentials buildDeviceCredentialsForUplinkMsg(DeviceId deviceId) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsValue("NEW_TOKEN"); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + return deviceCredentials; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 9d48824bed..325872d919 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.edge; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; @@ -44,6 +42,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.Abstrac import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -82,11 +81,10 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + DeviceProfile deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); - Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(thermostatsRuleChainId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()); - Assert.assertEquals(thermostatsRuleChainId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); // update device profile edgeImitator.expectMessageAmount(1); @@ -108,12 +106,9 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; - Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdMSB()); - Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdLSB()); - Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdMSB()); - Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdLSB()); - Assert.assertEquals(thermostatsDashboardId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdMSB()); - Assert.assertEquals(thermostatsDashboardId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); + deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); // delete profile edgeImitator.expectMessageAmount(1); @@ -138,16 +133,13 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + DeviceProfile deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); - Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(DeviceTransportType.SNMP.name(), deviceProfileUpdateMsg.getTransportType()); + Assert.assertEquals(DeviceTransportType.SNMP, deviceProfileMsg.getTransportType()); - Optional deviceProfileDataOpt = - dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); - - Assert.assertTrue(deviceProfileDataOpt.isPresent()); - DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + DeviceProfileData deviceProfileData = deviceProfileMsg.getProfileData(); Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof SnmpDeviceProfileTransportConfiguration); SnmpDeviceProfileTransportConfiguration transportConfiguration = @@ -179,16 +171,13 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + DeviceProfile deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); - Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(DeviceTransportType.LWM2M.name(), deviceProfileUpdateMsg.getTransportType()); + Assert.assertEquals(DeviceTransportType.LWM2M, deviceProfileMsg.getTransportType()); - Optional deviceProfileDataOpt = - dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); - - Assert.assertTrue(deviceProfileDataOpt.isPresent()); - DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + DeviceProfileData deviceProfileData = deviceProfileMsg.getProfileData(); Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof Lwm2mDeviceProfileTransportConfiguration); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = @@ -231,16 +220,13 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + DeviceProfile deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); - Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(DeviceTransportType.COAP.name(), deviceProfileUpdateMsg.getTransportType()); - - Optional deviceProfileDataOpt = - dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + Assert.assertEquals(DeviceTransportType.COAP, deviceProfileMsg.getTransportType()); - Assert.assertTrue(deviceProfileDataOpt.isPresent()); - DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + DeviceProfileData deviceProfileData = deviceProfileMsg.getProfileData(); Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof CoapDeviceProfileTransportConfiguration); CoapDeviceProfileTransportConfiguration transportConfiguration = @@ -275,20 +261,15 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Device Profile Rule Chain"); DashboardId dashboardId = createDashboardAndAssignToEdge("Device Profile Dashboard"); - UUID uuid = Uuids.timeBased(); + DeviceProfile deviceProfileMsg = buildDeviceProfileForUplinkMsg("Device Profile On Edge"); + deviceProfileMsg.setDefaultRuleChainId(ruleChainId); + deviceProfileMsg.setDefaultDashboardId(dashboardId); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder(); - deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - deviceProfileUpdateMsgBuilder.setName("Device Profile On Edge"); - deviceProfileUpdateMsgBuilder.setDefault(false); - deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name()); - deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(createProfileData()))); - deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()); - deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); - deviceProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); - deviceProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdMSB(deviceProfileMsg.getUuidId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdLSB(deviceProfileMsg.getUuidId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceProfileMsg)); deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsgBuilder); uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build()); @@ -303,7 +284,7 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); Assert.assertTrue(latestResponseMsg.getSuccess()); - DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + uuid, DeviceProfile.class); + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileMsg.getUuidId(), DeviceProfile.class); Assert.assertNotNull(deviceProfile); Assert.assertEquals("Device Profile On Edge", deviceProfile.getName()); @@ -422,16 +403,15 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { deviceProfileOnCloud = doPost("/api/deviceProfile", deviceProfileOnCloud, DeviceProfile.class); Assert.assertTrue(edgeImitator.waitForMessages()); - UUID uuid = Uuids.timeBased(); + DeviceProfile deviceProfileMsg = buildDeviceProfileForUplinkMsg(deviceProfileOnCloudName); + deviceProfileMsg.setProfileData(deviceProfileOnCloud.getProfileData()); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder(); - deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - deviceProfileUpdateMsgBuilder.setName(deviceProfileOnCloudName); - deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name()); + deviceProfileUpdateMsgBuilder.setIdMSB(deviceProfileMsg.getUuidId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdLSB(deviceProfileMsg.getUuidId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceProfileMsg)); deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); - deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfileOnCloud.getProfileData()))); uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build()); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); @@ -447,12 +427,26 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { Optional deviceProfileUpdateMsgOpt = edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class); Assert.assertTrue(deviceProfileUpdateMsgOpt.isPresent()); DeviceProfileUpdateMsg latestDeviceProfileUpdateMsg = deviceProfileUpdateMsgOpt.get(); - Assert.assertNotEquals(deviceProfileOnCloudName, latestDeviceProfileUpdateMsg.getName()); + deviceProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(latestDeviceProfileUpdateMsg.getEntity(), DeviceProfile.class); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertNotEquals(deviceProfileOnCloudName, deviceProfileMsg.getName()); - Assert.assertNotEquals(deviceProfileOnCloud.getUuidId(), uuid); + Assert.assertNotEquals(deviceProfileOnCloud.getId(), deviceProfileMsg.getId()); - DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + uuid, DeviceProfile.class); + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileMsg.getUuidId(), DeviceProfile.class); Assert.assertNotNull(deviceProfile); Assert.assertNotEquals(deviceProfileOnCloudName, deviceProfile.getName()); } + + private DeviceProfile buildDeviceProfileForUplinkMsg(String name) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(new DeviceProfileId(UUID.randomUUID())); + deviceProfile.setTenantId(tenantId); + deviceProfile.setName(name); + deviceProfile.setDefault(false); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setProfileData(createProfileData()); + return deviceProfile; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java index fd34b0d973..7330e2f5fc 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -54,10 +55,9 @@ public class EdgeTest extends AbstractEdgeTest { Optional customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); Assert.assertTrue(customerUpdateOpt.isPresent()); CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); + Customer customerMsg = JacksonUtil.fromStringIgnoreUnknownProperties(customerUpdateMsg.getEntity(), Customer.class); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB()); - Assert.assertEquals(savedCustomer.getTitle(), customerUpdateMsg.getTitle()); + Assert.assertEquals(savedCustomer, customerMsg); // unassign edge from customer edgeImitator.expectMessageAmount(2); diff --git a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java index d56ded7014..903f49f756 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.edge; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; @@ -30,10 +30,10 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.EdgeEntityType; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -68,8 +68,10 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + EntityView entityView = JacksonUtil.fromStringIgnoreUnknownProperties(entityViewUpdateMsg.getEntity(), EntityView.class); + Assert.assertNotNull(entityView); + Assert.assertEquals(savedEntityView, entityView); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); - Assert.assertEquals(savedEntityView.getName(), entityViewUpdateMsg.getName()); // request entity view(s) for device UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); @@ -131,9 +133,10 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + EntityView entityViewMsg = JacksonUtil.fromStringIgnoreUnknownProperties(entityViewUpdateMsg.getEntity(), EntityView.class); + Assert.assertNotNull(entityViewMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getCustomerIdMSB()); - Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getCustomerIdLSB()); + Assert.assertEquals(savedCustomer.getId(), entityViewMsg.getCustomerId()); // unassign entity view #2 from customer edgeImitator.expectMessageAmount(1); @@ -142,10 +145,10 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + entityViewMsg = JacksonUtil.fromStringIgnoreUnknownProperties(entityViewUpdateMsg.getEntity(), EntityView.class); + Assert.assertNotNull(entityViewMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); - Assert.assertEquals( - new CustomerId(EntityId.NULL_UUID), - new CustomerId(new UUID(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()))); + Assert.assertEquals(new CustomerId(EntityId.NULL_UUID), entityViewMsg.getCustomerId()); // delete entity view #2 - messages expected edgeImitator.expectMessageAmount(1); @@ -165,17 +168,13 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { public void testSendEntityViewToCloud() throws Exception { Device device = findDeviceByName("Edge Device 1"); - UUID uuid = Uuids.timeBased(); + EntityView entityViewMsg = buildEntityViewForUplinkMsg(device, "Edge EntityView 2"); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); - entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - entityViewUpdateMsgBuilder.setName("Edge EntityView 2"); - entityViewUpdateMsgBuilder.setType("test"); - entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); - entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); - entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setIdMSB(entityViewMsg.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(entityViewMsg.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setEntity(JacksonUtil.toString(entityViewMsg)); entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); @@ -192,7 +191,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); Assert.assertTrue(latestResponseMsg.getSuccess()); - EntityView entityView = doGet("/api/entityView/" + uuid, EntityView.class); + EntityView entityView = doGet("/api/entityView/" + entityViewMsg.getUuidId(), EntityView.class); Assert.assertNotNull(entityView); Assert.assertEquals("Edge EntityView 2", entityView.getName()); } @@ -204,17 +203,13 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { String entityViewOnCloudName = StringUtils.randomAlphanumeric(15); EntityView entityViewOnCloud = saveEntityView(entityViewOnCloudName, device.getId()); - UUID uuid = Uuids.timeBased(); + EntityView entityViewMsg = buildEntityViewForUplinkMsg(device, entityViewOnCloudName); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); - entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); - entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - entityViewUpdateMsgBuilder.setName(entityViewOnCloudName); - entityViewUpdateMsgBuilder.setType("test"); - entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); - entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); - entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setIdMSB(entityViewMsg.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(entityViewMsg.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setEntity(JacksonUtil.toString(entityViewMsg)); entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); @@ -232,13 +227,12 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { Optional entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class); Assert.assertTrue(entityViewUpdateMsgOpt.isPresent()); EntityViewUpdateMsg latestEntityViewUpdateMsg = entityViewUpdateMsgOpt.get(); - Assert.assertNotEquals(entityViewOnCloudName, latestEntityViewUpdateMsg.getName()); - - UUID newEntityViewId = new UUID(latestEntityViewUpdateMsg.getIdMSB(), latestEntityViewUpdateMsg.getIdLSB()); - - Assert.assertNotEquals(entityViewOnCloud.getId().getId(), newEntityViewId); + entityViewMsg = JacksonUtil.fromStringIgnoreUnknownProperties(latestEntityViewUpdateMsg.getEntity(), EntityView.class); + Assert.assertNotNull(entityViewMsg); + Assert.assertNotEquals(entityViewOnCloudName, entityViewMsg.getName()); + Assert.assertNotEquals(entityViewOnCloud.getId(), entityViewMsg.getId()); - EntityView entityView = doGet("/api/entityView/" + newEntityViewId, EntityView.class); + EntityView entityView = doGet("/api/entityView/" + entityViewMsg.getUuidId(), EntityView.class); Assert.assertNotNull(entityView); Assert.assertNotEquals(entityViewOnCloudName, entityView.getName()); } @@ -274,14 +268,11 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + EntityView entityViewMsg = JacksonUtil.fromStringIgnoreUnknownProperties(entityViewUpdateMsg.getEntity(), EntityView.class); + Assert.assertNotNull(entityViewMsg); + Assert.assertEquals(entityView, entityViewMsg); + Assert.assertEquals(device.getId(), entityViewMsg.getEntityId()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); - Assert.assertEquals(entityView.getType(), entityViewUpdateMsg.getType()); - Assert.assertEquals(entityView.getName(), entityViewUpdateMsg.getName()); - Assert.assertEquals(entityView.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); - Assert.assertEquals(entityView.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); - Assert.assertEquals(device.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getEntityIdMSB()); - Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getEntityIdLSB()); - Assert.assertEquals(device.getId().getEntityType().name(), entityViewUpdateMsg.getEntityType().name()); testAutoGeneratedCodeByProtobuf(entityViewUpdateMsg); } @@ -309,4 +300,15 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { return doPost("/api/entityView", entityView, EntityView.class); } + + private EntityView buildEntityViewForUplinkMsg(Device device, String name) { + EntityView entityView = new EntityView(); + entityView.setId(new EntityViewId(UUID.randomUUID())); + entityView.setTenantId(tenantId); + entityView.setName(name); + entityView.setType("test"); + entityView.setEntityId(device.getId()); + return entityView; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/OtaPackageEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OtaPackageEdgeTest.java index 5525d63f41..0ff72b94ec 100644 --- a/application/src/test/java/org/thingsboard/server/edge/OtaPackageEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/OtaPackageEdgeTest.java @@ -16,17 +16,16 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -59,22 +58,22 @@ public class OtaPackageEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof OtaPackageUpdateMsg); OtaPackageUpdateMsg otaPackageUpdateMsg = (OtaPackageUpdateMsg) latestMessage; + OtaPackage otaPackage = JacksonUtil.fromStringIgnoreUnknownProperties(otaPackageUpdateMsg.getEntity(), OtaPackage.class); + Assert.assertNotNull(otaPackage); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, otaPackageUpdateMsg.getMsgType()); - Assert.assertEquals(savedFirmwareInfo.getUuidId().getMostSignificantBits(), otaPackageUpdateMsg.getIdMSB()); - Assert.assertEquals(savedFirmwareInfo.getUuidId().getLeastSignificantBits(), otaPackageUpdateMsg.getIdLSB()); - Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), otaPackageUpdateMsg.getDeviceProfileIdMSB()); - Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), otaPackageUpdateMsg.getDeviceProfileIdLSB()); - Assert.assertEquals(FIRMWARE, OtaPackageType.valueOf(otaPackageUpdateMsg.getType())); - Assert.assertEquals("My firmware #1", otaPackageUpdateMsg.getTitle()); - Assert.assertEquals("v1.0", otaPackageUpdateMsg.getVersion()); - Assert.assertEquals("My firmware #1 v1.0", otaPackageUpdateMsg.getTag()); - Assert.assertEquals("http://localhost:8080/v1/package", otaPackageUpdateMsg.getUrl()); - Assert.assertFalse(otaPackageUpdateMsg.hasData()); - Assert.assertFalse(otaPackageUpdateMsg.hasFileName()); - Assert.assertFalse(otaPackageUpdateMsg.hasContentType()); - Assert.assertFalse(otaPackageUpdateMsg.hasChecksumAlgorithm()); - Assert.assertFalse(otaPackageUpdateMsg.hasChecksum()); - Assert.assertFalse(otaPackageUpdateMsg.hasDataSize()); + Assert.assertEquals(savedFirmwareInfo.getId(), otaPackage.getId()); + Assert.assertEquals(thermostatDeviceProfile.getId(), otaPackage.getDeviceProfileId()); + Assert.assertEquals(FIRMWARE, otaPackage.getType()); + Assert.assertEquals("My firmware #1", otaPackage.getTitle()); + Assert.assertEquals("v1.0", otaPackage.getVersion()); + Assert.assertEquals("My firmware #1 v1.0", otaPackage.getTag()); + Assert.assertEquals("http://localhost:8080/v1/package", otaPackage.getUrl()); + Assert.assertNull(otaPackage.getData()); + Assert.assertNull(otaPackage.getFileName()); + Assert.assertNull(otaPackage.getContentType()); + Assert.assertNull(otaPackage.getChecksumAlgorithm()); + Assert.assertNull(otaPackage.getChecksum()); + Assert.assertNull(otaPackage.getDataSize()); // delete ota package edgeImitator.expectMessageAmount(1); @@ -113,22 +112,22 @@ public class OtaPackageEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof OtaPackageUpdateMsg); OtaPackageUpdateMsg otaPackageUpdateMsg = (OtaPackageUpdateMsg) latestMessage; + OtaPackage otaPackage = JacksonUtil.fromStringIgnoreUnknownProperties(otaPackageUpdateMsg.getEntity(), OtaPackage.class); + Assert.assertNotNull(otaPackage); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, otaPackageUpdateMsg.getMsgType()); - Assert.assertEquals(savedFirmwareInfo.getUuidId().getMostSignificantBits(), otaPackageUpdateMsg.getIdMSB()); - Assert.assertEquals(savedFirmwareInfo.getUuidId().getLeastSignificantBits(), otaPackageUpdateMsg.getIdLSB()); - Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), otaPackageUpdateMsg.getDeviceProfileIdMSB()); - Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), otaPackageUpdateMsg.getDeviceProfileIdLSB()); - Assert.assertEquals(FIRMWARE, OtaPackageType.valueOf(otaPackageUpdateMsg.getType())); - Assert.assertEquals("My firmware #2", otaPackageUpdateMsg.getTitle()); - Assert.assertEquals("v2.0", otaPackageUpdateMsg.getVersion()); - Assert.assertEquals("My firmware #2 v2.0", otaPackageUpdateMsg.getTag()); - Assert.assertFalse(otaPackageUpdateMsg.hasUrl()); - Assert.assertEquals("firmware.bin", otaPackageUpdateMsg.getFileName()); - Assert.assertEquals("image/png", otaPackageUpdateMsg.getContentType()); - Assert.assertEquals(ChecksumAlgorithm.SHA256.name(), otaPackageUpdateMsg.getChecksumAlgorithm()); - Assert.assertEquals("62467691cf583d4fa78b18fafaf9801f505e0ef03baf0603fd4b0cd004cd1e75", otaPackageUpdateMsg.getChecksum()); - Assert.assertEquals(3L, otaPackageUpdateMsg.getDataSize()); - Assert.assertEquals(ByteString.copyFrom(new byte[]{1, 3, 5}), otaPackageUpdateMsg.getData()); + Assert.assertEquals(savedFirmwareInfo.getId(), otaPackage.getId()); + Assert.assertEquals(thermostatDeviceProfile.getId(), otaPackage.getDeviceProfileId()); + Assert.assertEquals(FIRMWARE, otaPackage.getType()); + Assert.assertEquals("My firmware #2", otaPackage.getTitle()); + Assert.assertEquals("v2.0", otaPackage.getVersion()); + Assert.assertEquals("My firmware #2 v2.0", otaPackage.getTag()); + Assert.assertFalse(otaPackage.hasUrl()); + Assert.assertEquals("firmware.bin", otaPackage.getFileName()); + Assert.assertEquals("image/png", otaPackage.getContentType()); + Assert.assertEquals(ChecksumAlgorithm.SHA256, otaPackage.getChecksumAlgorithm()); + Assert.assertEquals("62467691cf583d4fa78b18fafaf9801f505e0ef03baf0603fd4b0cd004cd1e75", otaPackage.getChecksum()); + Assert.assertEquals(3L, otaPackage.getDataSize().longValue()); + Assert.assertEquals(ByteBuffer.wrap(new byte[]{1, 3, 5}), otaPackage.getData()); // delete ota package edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/QueueEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/QueueEdgeTest.java index 32e8e7f222..b80a7de112 100644 --- a/application/src/test/java/org/thingsboard/server/edge/QueueEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/QueueEdgeTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; @@ -63,24 +64,22 @@ public class QueueEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof QueueUpdateMsg); QueueUpdateMsg queueUpdateMsg = (QueueUpdateMsg) latestMessage; + Queue queueMsg = JacksonUtil.fromStringIgnoreUnknownProperties(queueUpdateMsg.getEntity(), Queue.class); + Assert.assertNotNull(queueMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, queueUpdateMsg.getMsgType()); - Assert.assertEquals(savedQueue.getUuidId().getMostSignificantBits(), queueUpdateMsg.getIdMSB()); - Assert.assertEquals(savedQueue.getUuidId().getLeastSignificantBits(), queueUpdateMsg.getIdLSB()); - Assert.assertEquals(savedQueue.getTenantId().getId().getMostSignificantBits(), queueUpdateMsg.getTenantIdMSB()); - Assert.assertEquals(savedQueue.getTenantId().getId().getLeastSignificantBits(), queueUpdateMsg.getTenantIdLSB()); - Assert.assertEquals("EdgeMain", queueUpdateMsg.getName()); - Assert.assertEquals("tb_rule_engine.EdgeMain", queueUpdateMsg.getTopic()); - Assert.assertEquals(25, queueUpdateMsg.getPollInterval()); - Assert.assertEquals(10, queueUpdateMsg.getPartitions()); - Assert.assertFalse(queueUpdateMsg.getConsumerPerPartition()); - Assert.assertEquals(2000, queueUpdateMsg.getPackProcessingTimeout()); - Assert.assertEquals(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR.name(), queueUpdateMsg.getSubmitStrategy().getType()); - Assert.assertEquals(0, queueUpdateMsg.getSubmitStrategy().getBatchSize()); - Assert.assertEquals(ProcessingStrategyType.RETRY_ALL.name(), queueUpdateMsg.getProcessingStrategy().getType()); - Assert.assertEquals(3, queueUpdateMsg.getProcessingStrategy().getRetries()); - Assert.assertEquals(0.7, queueUpdateMsg.getProcessingStrategy().getFailurePercentage(), 1); - Assert.assertEquals(3, queueUpdateMsg.getProcessingStrategy().getPauseBetweenRetries()); - Assert.assertEquals(5, queueUpdateMsg.getProcessingStrategy().getMaxPauseBetweenRetries()); + Assert.assertEquals(savedQueue, queueMsg); + Assert.assertEquals("EdgeMain", queueMsg.getName()); + Assert.assertEquals("tb_rule_engine.EdgeMain", queueMsg.getTopic()); + Assert.assertEquals(25, queueMsg.getPollInterval()); + Assert.assertEquals(10, queueMsg.getPartitions()); + Assert.assertEquals(2000, queueMsg.getPackProcessingTimeout()); + Assert.assertEquals(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR, queueMsg.getSubmitStrategy().getType()); + Assert.assertEquals(0, queueMsg.getSubmitStrategy().getBatchSize()); + Assert.assertEquals(ProcessingStrategyType.RETRY_ALL, queueMsg.getProcessingStrategy().getType()); + Assert.assertEquals(3, queueMsg.getProcessingStrategy().getRetries()); + Assert.assertEquals(0.7, queueMsg.getProcessingStrategy().getFailurePercentage(), 1); + Assert.assertEquals(3, queueMsg.getProcessingStrategy().getPauseBetweenRetries()); + Assert.assertEquals(5, queueMsg.getProcessingStrategy().getMaxPauseBetweenRetries()); // update queue edgeImitator.expectMessageAmount(1); @@ -90,8 +89,10 @@ public class QueueEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof QueueUpdateMsg); queueUpdateMsg = (QueueUpdateMsg) latestMessage; + queueMsg = JacksonUtil.fromStringIgnoreUnknownProperties(queueUpdateMsg.getEntity(), Queue.class); + Assert.assertNotNull(queueMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, queueUpdateMsg.getMsgType()); - Assert.assertEquals(50, queueUpdateMsg.getPollInterval()); + Assert.assertEquals(50, queueMsg.getPollInterval()); // delete queue edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java index 04e6f3a3fb..5fc532ece5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.edge; +import com.fasterxml.jackson.databind.node.TextNode; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -30,8 +31,6 @@ import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; -import java.util.UUID; - import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -54,15 +53,10 @@ public class RelationEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + EntityRelation entityRelation = JacksonUtil.fromStringIgnoreUnknownProperties(relationUpdateMsg.getEntity(), EntityRelation.class); + Assert.assertNotNull(entityRelation); + Assert.assertEquals(relation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); - Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); - Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); - Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); - Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); // delete relation edgeImitator.expectMessageAmount(1); @@ -78,15 +72,10 @@ public class RelationEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); relationUpdateMsg = (RelationUpdateMsg) latestMessage; + entityRelation = JacksonUtil.fromStringIgnoreUnknownProperties(relationUpdateMsg.getEntity(), EntityRelation.class); + Assert.assertNotNull(entityRelation); + Assert.assertEquals(relation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); - Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); - Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); - Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); - Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); - Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); } @Test @@ -94,17 +83,11 @@ public class RelationEdgeTest extends AbstractEdgeTest { Device device1 = saveDeviceOnCloudAndVerifyDeliveryToEdge(); Device device2 = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + EntityRelation entityRelation = buildEntityRelationForUplinkMsg(device1.getId(), device2.getId()); + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); RelationUpdateMsg.Builder relationUpdateMsgBuilder = RelationUpdateMsg.newBuilder(); - relationUpdateMsgBuilder.setType("test"); - relationUpdateMsgBuilder.setTypeGroup(RelationTypeGroup.COMMON.name()); - relationUpdateMsgBuilder.setToIdMSB(device1.getId().getId().getMostSignificantBits()); - relationUpdateMsgBuilder.setToIdLSB(device1.getId().getId().getLeastSignificantBits()); - relationUpdateMsgBuilder.setToEntityType(device1.getId().getEntityType().name()); - relationUpdateMsgBuilder.setFromIdMSB(device2.getId().getId().getMostSignificantBits()); - relationUpdateMsgBuilder.setFromIdLSB(device2.getId().getId().getLeastSignificantBits()); - relationUpdateMsgBuilder.setFromEntityType(device2.getId().getEntityType().name()); - relationUpdateMsgBuilder.setAdditionalInfo("{}"); + relationUpdateMsgBuilder.setEntity(JacksonUtil.toString(entityRelation)); testAutoGeneratedCodeByProtobuf(relationUpdateMsgBuilder); uplinkMsgBuilder.addRelationUpdateMsg(relationUpdateMsgBuilder.build()); @@ -129,14 +112,24 @@ public class RelationEdgeTest extends AbstractEdgeTest { 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); + EntityRelation deviceToAssetRelation = new EntityRelation(); + deviceToAssetRelation.setType("test"); + deviceToAssetRelation.setFrom(device.getId()); + deviceToAssetRelation.setTo(asset.getId()); + deviceToAssetRelation.setTypeGroup(RelationTypeGroup.COMMON); edgeImitator.expectMessageAmount(1); - doPost("/api/relation", relation); + doPost("/api/relation", deviceToAssetRelation); + Assert.assertTrue(edgeImitator.waitForMessages()); + + EntityRelation assetToTenantRelation = new EntityRelation(); + assetToTenantRelation.setType("test"); + assetToTenantRelation.setFrom(asset.getId()); + assetToTenantRelation.setTo(tenantId); + assetToTenantRelation.setTypeGroup(RelationTypeGroup.COMMON); + + edgeImitator.expectMessageAmount(1); + doPost("/api/relation", assetToTenantRelation); Assert.assertTrue(edgeImitator.waitForMessages()); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); @@ -158,17 +151,19 @@ public class RelationEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + EntityRelation entityRelation = JacksonUtil.fromStringIgnoreUnknownProperties(relationUpdateMsg.getEntity(), EntityRelation.class); + Assert.assertNotNull(entityRelation); + Assert.assertEquals(deviceToAssetRelation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); - Assert.assertEquals(relation.getType(), relationUpdateMsg.getType()); - - UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); - EntityId fromEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getFromEntityType(), fromUUID); - Assert.assertEquals(relation.getFrom(), fromEntityId); - - UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); - EntityId toEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getToEntityType(), toUUID); - Assert.assertEquals(relation.getTo(), toEntityId); + } - Assert.assertEquals(relation.getTypeGroup().name(), relationUpdateMsg.getTypeGroup()); + private EntityRelation buildEntityRelationForUplinkMsg(DeviceId deviceId1, DeviceId deviceId2) { + EntityRelation relation = new EntityRelation(); + relation.setType("test"); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setTo(deviceId1); + relation.setFrom(deviceId2); + relation.setAdditionalInfo(TextNode.valueOf("{}")); + return relation; } } diff --git a/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java index 2ae57f7f19..eaf004ca51 100644 --- a/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java @@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; @@ -46,7 +47,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest { resource.setResourceType(ResourceType.JKS); resource.setTitle("Edge Test Resource"); resource.setFileName(FILE_NAME); - resource.setData(TEST_DATA); + resource.setEncodedData(TEST_DATA); edgeImitator.expectMessageAmount(1); TbResource savedResource = doPost("/api/resource", resource, TbResource.class); @@ -58,12 +59,14 @@ public class ResourceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, resourceUpdateMsg.getMsgType()); Assert.assertEquals(savedResource.getUuidId().getMostSignificantBits(), resourceUpdateMsg.getIdMSB()); Assert.assertEquals(savedResource.getUuidId().getLeastSignificantBits(), resourceUpdateMsg.getIdLSB()); - Assert.assertEquals("Edge Test Resource", resourceUpdateMsg.getTitle()); - Assert.assertEquals(ResourceType.JKS.name(), resourceUpdateMsg.getResourceType()); - Assert.assertEquals(FILE_NAME, resourceUpdateMsg.getResourceKey()); - Assert.assertEquals(FILE_NAME, resourceUpdateMsg.getFileName()); - Assert.assertEquals(TEST_DATA, resourceUpdateMsg.getData()); - Assert.assertTrue(StringUtils.isNotBlank(resourceUpdateMsg.getEtag())); + TbResource tbResource = JacksonUtil.fromStringIgnoreUnknownProperties(resourceUpdateMsg.getEntity(), TbResource.class); + Assert.assertNotNull(tbResource); + Assert.assertEquals("Edge Test Resource", tbResource.getTitle()); + Assert.assertEquals(ResourceType.JKS, tbResource.getResourceType()); + Assert.assertEquals(FILE_NAME, tbResource.getResourceKey()); + Assert.assertEquals(FILE_NAME, tbResource.getFileName()); + Assert.assertEquals(TEST_DATA, tbResource.getEncodedData()); + Assert.assertTrue(StringUtils.isNotBlank(tbResource.getEtag())); // update resource edgeImitator.expectMessageAmount(1); @@ -73,8 +76,10 @@ public class ResourceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof ResourceUpdateMsg); resourceUpdateMsg = (ResourceUpdateMsg) latestMessage; + tbResource = JacksonUtil.fromStringIgnoreUnknownProperties(resourceUpdateMsg.getEntity(), TbResource.class); + Assert.assertNotNull(tbResource); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, resourceUpdateMsg.getMsgType()); - Assert.assertEquals("Updated Edge Test Resource", resourceUpdateMsg.getTitle()); + Assert.assertEquals("Updated Edge Test Resource", tbResource.getTitle()); // delete resource edgeImitator.expectMessageAmount(1); @@ -91,18 +96,14 @@ public class ResourceEdgeTest extends AbstractEdgeTest { @Test public void testSendResourceToCloud() throws Exception { + TbResource tbResource = createTbResource(); UUID uuid = Uuids.timeBased(); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder(); resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - resourceUpdateMsgBuilder.setTitle("Edge Test Resource"); - resourceUpdateMsgBuilder.setResourceType(ResourceType.JKS.name()); - resourceUpdateMsgBuilder.setResourceKey(FILE_NAME); - resourceUpdateMsgBuilder.setFileName(FILE_NAME); - resourceUpdateMsgBuilder.setData(TEST_DATA); - resourceUpdateMsgBuilder.setIsSystem(false); + resourceUpdateMsgBuilder.setEntity(JacksonUtil.toString(tbResource)); resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder); uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build()); @@ -117,18 +118,15 @@ public class ResourceEdgeTest extends AbstractEdgeTest { UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); Assert.assertTrue(latestResponseMsg.getSuccess()); - TbResource tbResource = doGet("/api/resource/" + uuid, TbResource.class); - Assert.assertNotNull(tbResource); - Assert.assertEquals("Edge Test Resource", tbResource.getName()); + TbResource tb = doGet("/api/resource/" + uuid, TbResource.class); + Assert.assertNotNull(tb); + Assert.assertEquals("Edge Test Resource", tb.getName()); + Assert.assertEquals(TEST_DATA, tb.getEncodedData()); } @Test public void testResourceToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { - TbResource resource = new TbResource(); - resource.setResourceType(ResourceType.JKS); - resource.setTitle("Edge Test Resource"); - resource.setFileName(FILE_NAME); - resource.setData(TEST_DATA); + TbResource resource = createTbResource(); edgeImitator.expectMessageAmount(1); TbResource savedResource = doPost("/api/resource", resource, TbResource.class); @@ -140,12 +138,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest { ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder(); resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); - resourceUpdateMsgBuilder.setTitle("Edge Test Resource"); - resourceUpdateMsgBuilder.setResourceType(ResourceType.JKS.name()); - resourceUpdateMsgBuilder.setResourceKey(FILE_NAME); - resourceUpdateMsgBuilder.setFileName(FILE_NAME); - resourceUpdateMsgBuilder.setData(TEST_DATA); - resourceUpdateMsgBuilder.setIsSystem(false); + resourceUpdateMsgBuilder.setEntity(JacksonUtil.toString(resource)); resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder); uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build()); @@ -171,4 +164,15 @@ public class ResourceEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(tbResource); Assert.assertNotEquals(FILE_NAME, tbResource.getName()); } + + private TbResource createTbResource() { + TbResource tbResource = new TbResource(); + tbResource.setTenantId(tenantId); + tbResource.setTitle("Edge Test Resource"); + tbResource.setResourceType(ResourceType.JKS); + tbResource.setResourceKey(FILE_NAME); + tbResource.setFileName(FILE_NAME); + tbResource.setEncodedData(TEST_DATA); + return tbResource; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 848b115c72..ff87a64bfe 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -19,7 +19,6 @@ import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.edge.Edge; @@ -32,7 +31,6 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleNodeProto; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -40,7 +38,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -64,11 +61,12 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Optional ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); + RuleChain ruleChainMsg = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainUpdateMsg.getEntity(), RuleChain.class); + Assert.assertNotNull(ruleChainMsg); Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) || UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType())); - Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); - Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); - Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); + Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); + Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName()); testRuleChainMetadataRequestMsg(savedRuleChain.getId()); @@ -113,8 +111,9 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), edgeRootRuleChainId.getId().getMostSignificantBits()); - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdLSB(), edgeRootRuleChainId.getId().getLeastSignificantBits()); + RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class); + Assert.assertNotNull(ruleChainMetadataMsg); + Assert.assertEquals(edgeRootRuleChainId, ruleChainMetadataMsg.getRuleChainId()); testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg); } @@ -138,12 +137,12 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - RuleChainId receivedRuleChainId = - new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); - Assert.assertEquals(ruleChainId, receivedRuleChainId); + RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class); + Assert.assertNotNull(ruleChainMetadataMsg); + Assert.assertEquals(ruleChainId, ruleChainMetadataMsg.getRuleChainId()); - for (RuleNodeProto ruleNodeProto : ruleChainMetadataUpdateMsg.getNodesList()) { - Assert.assertEquals(CONFIGURATION_VERSION, ruleNodeProto.getConfigurationVersion()); + for (RuleNode ruleNode : ruleChainMetadataMsg.getNodes()) { + Assert.assertEquals(CONFIGURATION_VERSION, ruleNode.getConfigurationVersion()); } } @@ -208,10 +207,11 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Optional ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); + RuleChain ruleChainMsg = JacksonUtil.fromStringIgnoreUnknownProperties(ruleChainUpdateMsg.getEntity(), RuleChain.class); + Assert.assertNotNull(ruleChainMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); - Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); - Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); - Assert.assertTrue(ruleChainUpdateMsg.getRoot()); + Assert.assertTrue(ruleChainMsg.isRoot()); + Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); // revert root rule chain edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java index 95fdc9f56a..4c61b0d91d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java @@ -17,16 +17,15 @@ package org.thingsboard.server.edge; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.Optional; -import java.util.UUID; @DaoSqlTest public class TenantEdgeTest extends AbstractEdgeTest { @@ -49,15 +48,14 @@ public class TenantEdgeTest extends AbstractEdgeTest { Optional tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + Tenant tenantMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantUpdateMsg.getEntity(), Tenant.class); + Assert.assertNotNull(tenantMsg); + Assert.assertEquals(savedTenant, tenantMsg); + TenantProfile tenantProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfileMsg); + Assert.assertEquals(tenantMsg.getTenantProfileId(), tenantProfileMsg.getId()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB()); - Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle()); - Assert.assertEquals("Updated Title for Tenant Edge Test", tenantUpdateMsg.getTitle()); - Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId - (new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB()))); - Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId - (new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()))); + Assert.assertEquals("Updated Title for Tenant Edge Test", tenantMsg.getTitle()); //change tenant profile for tenant TenantProfile tenantProfile = createTenantProfile(); @@ -68,18 +66,17 @@ public class TenantEdgeTest extends AbstractEdgeTest { tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class); Assert.assertTrue(tenantUpdateMsgOpt.isPresent()); tenantUpdateMsg = tenantUpdateMsgOpt.get(); + tenantMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantUpdateMsg.getEntity(), Tenant.class); + Assert.assertNotNull(tenantMsg); tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + tenantProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfileMsg); // tenant update Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB()); - Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle()); - Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId - (new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB()))); - Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId - (new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()))); + Assert.assertEquals(savedTenant, tenantMsg); + Assert.assertEquals(savedTenant.getTenantProfileId(), tenantProfileMsg.getId()); } private TenantProfile createTenantProfile() { diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java index 6f665ce462..e958325410 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java @@ -18,10 +18,12 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @@ -55,12 +57,12 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof TenantProfileUpdateMsg); TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) latestMessage; + TenantProfile tenantProfileMsg = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfileMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType()); - Assert.assertEquals(edgeTenantProfile.getUuidId().getMostSignificantBits(), tenantProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(edgeTenantProfile.getUuidId().getLeastSignificantBits(), tenantProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(edgeTenantProfile.getDescription(), tenantProfileUpdateMsg.getDescription()); - Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileUpdateMsg.getDescription()); - Assert.assertEquals("Tenant Profile Edge Test", tenantProfileUpdateMsg.getName()); + Assert.assertEquals(edgeTenantProfile, tenantProfileMsg); + Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileMsg.getDescription()); + Assert.assertEquals("Tenant Profile Edge Test", tenantProfileMsg.getName()); loginTenantAdmin(); } @@ -83,10 +85,11 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { Optional tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class); Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent()); TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get(); + TenantProfile tenantProfile = JacksonUtil.fromStringIgnoreUnknownProperties(tenantProfileUpdateMsg.getEntity(), TenantProfile.class); + Assert.assertNotNull(tenantProfile); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType()); - Assert.assertEquals(edgeTenantProfile.getUuidId().getMostSignificantBits(), tenantProfileUpdateMsg.getIdMSB()); - Assert.assertEquals(edgeTenantProfile.getUuidId().getLeastSignificantBits(), tenantProfileUpdateMsg.getIdLSB()); - Assert.assertEquals(edgeTenantProfile.getDescription(), tenantProfileUpdateMsg.getDescription()); + Assert.assertEquals(edgeTenantProfile.getId(), tenantProfile.getId()); + Assert.assertEquals(edgeTenantProfile.getDescription(), tenantProfile.getDescription()); List queueUpdateMsgs = edgeImitator.findAllMessagesByType(QueueUpdateMsg.class); Assert.assertEquals(2, queueUpdateMsgs.size()); @@ -103,8 +106,9 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { queueUpdateMsgs = edgeImitator.findAllMessagesByType(QueueUpdateMsg.class); Assert.assertEquals(2, queueUpdateMsgs.size()); for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { - Assert.assertEquals(tenantId.getId().getMostSignificantBits(), queueUpdateMsg.getTenantIdMSB()); - Assert.assertEquals(tenantId.getId().getLeastSignificantBits(), queueUpdateMsg.getTenantIdLSB()); + Queue queue = JacksonUtil.fromStringIgnoreUnknownProperties(queueUpdateMsg.getEntity(), Queue.class); + Assert.assertNotNull(queue); + Assert.assertEquals(tenantId, queue.getTenantId()); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 3dc398b9f3..8dd5b27ad5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -20,10 +20,12 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -57,13 +59,14 @@ public class UserEdgeTest extends AbstractEdgeTest { Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); + User userMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userUpdateMsg.getEntity(), User.class); + Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); - Assert.assertEquals(savedTenantAdmin.getAuthority().name(), userUpdateMsg.getAuthority()); - Assert.assertEquals(savedTenantAdmin.getEmail(), userUpdateMsg.getEmail()); - Assert.assertEquals(savedTenantAdmin.getFirstName(), userUpdateMsg.getFirstName()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userUpdateMsg.getLastName()); + Assert.assertEquals(savedTenantAdmin.getId(), userMsg.getId()); + Assert.assertEquals(savedTenantAdmin.getAuthority(), userMsg.getAuthority()); + Assert.assertEquals(savedTenantAdmin.getEmail(), userMsg.getEmail()); + Assert.assertEquals(savedTenantAdmin.getFirstName(), userMsg.getFirstName()); + Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); Optional userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); @@ -75,8 +78,10 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserUpdateMsg); userUpdateMsg = (UserUpdateMsg) latestMessage; + userMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userUpdateMsg.getEntity(), User.class); + Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userUpdateMsg.getLastName()); + Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); @@ -90,9 +95,10 @@ public class UserEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userCredentialsUpdateMsg.getUserIdMSB()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsUpdateMsg.getPassword())); + UserCredentials userCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userCredentialsUpdateMsg.getEntity(), UserCredentials.class); + Assert.assertNotNull(userCredentialsMsg); + Assert.assertEquals(savedTenantAdmin.getId(), userCredentialsMsg.getUserId()); + Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); loginTenantAdmin(); @@ -138,15 +144,15 @@ public class UserEdgeTest extends AbstractEdgeTest { Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); + User userMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userUpdateMsg.getEntity(), User.class); + Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); - Assert.assertEquals(savedCustomerUser.getCustomerId().getId().getMostSignificantBits(), userUpdateMsg.getCustomerIdMSB()); - Assert.assertEquals(savedCustomerUser.getCustomerId().getId().getLeastSignificantBits(), userUpdateMsg.getCustomerIdLSB()); - Assert.assertEquals(savedCustomerUser.getAuthority().name(), userUpdateMsg.getAuthority()); - Assert.assertEquals(savedCustomerUser.getEmail(), userUpdateMsg.getEmail()); - Assert.assertEquals(savedCustomerUser.getFirstName(), userUpdateMsg.getFirstName()); - Assert.assertEquals(savedCustomerUser.getLastName(), userUpdateMsg.getLastName()); + Assert.assertEquals(savedCustomerUser.getId(), userMsg.getId()); + Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); + Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); + Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); + Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); + Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); // update user edgeImitator.expectMessageAmount(1); @@ -156,8 +162,10 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserUpdateMsg); userUpdateMsg = (UserUpdateMsg) latestMessage; + userMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userUpdateMsg.getEntity(), User.class); + Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getLastName(), userUpdateMsg.getLastName()); + Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); // update user credentials login(savedCustomerUser.getEmail(), "customer"); @@ -171,9 +179,10 @@ public class UserEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userCredentialsUpdateMsg.getUserIdMSB()); - Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsUpdateMsg.getPassword())); + UserCredentials userCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userCredentialsUpdateMsg.getEntity(), UserCredentials.class); + Assert.assertNotNull(userCredentialsMsg); + Assert.assertEquals(savedCustomerUser.getId(), userCredentialsMsg.getUserId()); + Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); loginTenantAdmin(); @@ -211,8 +220,9 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(tenantAdminUserId.getId().getMostSignificantBits(), userCredentialsUpdateMsg.getUserIdMSB()); - Assert.assertEquals(tenantAdminUserId.getId().getLeastSignificantBits(), userCredentialsUpdateMsg.getUserIdLSB()); + UserCredentials userCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userCredentialsUpdateMsg.getEntity(), UserCredentials.class); + Assert.assertNotNull(userCredentialsMsg); + Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); } @Test @@ -235,8 +245,9 @@ public class UserEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - Assert.assertEquals(userCredentialsUpdateMsg.getUserIdMSB(), tenantAdminUserId.getId().getMostSignificantBits()); - Assert.assertEquals(userCredentialsUpdateMsg.getUserIdLSB(), tenantAdminUserId.getId().getLeastSignificantBits()); + UserCredentials userCredentialsMsg = JacksonUtil.fromStringIgnoreUnknownProperties(userCredentialsUpdateMsg.getEntity(), UserCredentials.class); + Assert.assertNotNull(userCredentialsMsg); + Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } diff --git a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java index e958741cd2..a325de3402 100644 --- a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java @@ -43,11 +43,12 @@ public class WidgetEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + WidgetsBundle widgetsBundleMsg = JacksonUtil.fromStringIgnoreUnknownProperties(widgetsBundleUpdateMsg.getEntity(), WidgetsBundle.class); + Assert.assertNotNull(widgetsBundleMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); - Assert.assertEquals(savedWidgetsBundle.getUuidId().getMostSignificantBits(), widgetsBundleUpdateMsg.getIdMSB()); - Assert.assertEquals(savedWidgetsBundle.getUuidId().getLeastSignificantBits(), widgetsBundleUpdateMsg.getIdLSB()); - Assert.assertEquals(savedWidgetsBundle.getAlias(), widgetsBundleUpdateMsg.getAlias()); - Assert.assertEquals(savedWidgetsBundle.getTitle(), widgetsBundleUpdateMsg.getTitle()); + Assert.assertEquals(savedWidgetsBundle.getId(), widgetsBundleMsg.getId()); + Assert.assertEquals(savedWidgetsBundle.getAlias(), widgetsBundleMsg.getAlias()); + Assert.assertEquals(savedWidgetsBundle.getTitle(), widgetsBundleMsg.getTitle()); testAutoGeneratedCodeByProtobuf(widgetsBundleUpdateMsg); // create widget type @@ -64,15 +65,10 @@ public class WidgetEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); WidgetTypeUpdateMsg widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + WidgetType widgetsType = JacksonUtil.fromStringIgnoreUnknownProperties(widgetTypeUpdateMsg.getEntity(), WidgetType.class); + Assert.assertNotNull(widgetsType); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); - Assert.assertEquals(savedWidgetType.getUuidId().getMostSignificantBits(), widgetTypeUpdateMsg.getIdMSB()); - Assert.assertEquals(savedWidgetType.getUuidId().getLeastSignificantBits(), widgetTypeUpdateMsg.getIdLSB()); - Assert.assertEquals(savedWidgetType.getFqn(), widgetTypeUpdateMsg.getFqn()); - Assert.assertEquals(savedWidgetType.getName(), widgetTypeUpdateMsg.getName()); - Assert.assertTrue(widgetTypeUpdateMsg.getDeprecated()); - Assert.assertEquals("bundle_alias", widgetTypeUpdateMsg.getBundleAlias()); - Assert.assertEquals("type_alias", widgetTypeUpdateMsg.getAlias()); - Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); + Assert.assertEquals(savedWidgetType, widgetsType); // update widget bundle edgeImitator.expectMessageAmount(1); @@ -82,8 +78,10 @@ public class WidgetEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + widgetsBundleMsg = JacksonUtil.fromStringIgnoreUnknownProperties(widgetsBundleUpdateMsg.getEntity(), WidgetsBundle.class); + Assert.assertNotNull(widgetsBundleMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); - Assert.assertEquals(savedWidgetsBundle.getTitle(), widgetsBundleUpdateMsg.getTitle()); + Assert.assertEquals(savedWidgetsBundle.getTitle(), widgetsBundleMsg.getTitle()); // update widget type edgeImitator.expectMessageAmount(1); @@ -93,8 +91,10 @@ public class WidgetEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + widgetsType = JacksonUtil.fromStringIgnoreUnknownProperties(widgetTypeUpdateMsg.getEntity(), WidgetType.class); + Assert.assertNotNull(widgetsType); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); - Assert.assertEquals(savedWidgetType.getName(), widgetTypeUpdateMsg.getName()); + Assert.assertEquals(savedWidgetType.getName(), widgetsType.getName()); // delete widget type edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java index c1f34217ac..c7a3e7a24c 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -24,11 +24,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -37,6 +37,7 @@ import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleNodeProto; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV1; import java.util.ArrayList; import java.util.List; @@ -49,13 +50,13 @@ public class RuleChainMsgConstructorTest { private static final String RPC_CONNECTION_TYPE = "RPC"; - private RuleChainMsgConstructor constructor; + private RuleChainMsgConstructorV1 ruleChainMsgConstructorV1; private TenantId tenantId; @Before public void setup() { - constructor = new RuleChainMsgConstructor(); + ruleChainMsgConstructorV1 = new RuleChainMsgConstructorV1(); tenantId = new TenantId(UUID.randomUUID()); } @@ -65,7 +66,7 @@ public class RuleChainMsgConstructorTest { RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - constructor.constructRuleChainMetadataUpdatedMsg( + ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( tenantId, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData, @@ -84,7 +85,7 @@ public class RuleChainMsgConstructorTest { RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - constructor.constructRuleChainMetadataUpdatedMsg( + ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( tenantId, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData, @@ -123,7 +124,7 @@ public class RuleChainMsgConstructorTest { RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - constructor.constructRuleChainMetadataUpdatedMsg( + ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( tenantId, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData, @@ -165,7 +166,7 @@ public class RuleChainMsgConstructorTest { RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - constructor.constructRuleChainMetadataUpdatedMsg( + ruleChainMsgConstructorV1.constructRuleChainMetadataUpdatedMsg( tenantId, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData1, diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index dc59d4f7e9..c6ee264783 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -60,27 +60,53 @@ import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.DataDecodingEncodingService; -import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.ResourceMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; -import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.asset.AssetMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.customer.CustomerMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.dashboard.DashboardMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.device.DeviceMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.entityview.EntityViewMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.ota.OtaPackageMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.queue.QueueMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.relation.RelationMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.resource.ResourceMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.settings.AdminSettingsMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.telemetry.EntityDataMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.tenant.TenantMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.user.UserMsgConstructorV2; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorFactory; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorV1; +import org.thingsboard.server.service.edge.rpc.constructor.widget.WidgetMsgConstructorV2; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -212,61 +238,139 @@ public abstract class BaseEdgeProcessorTest { protected EntityDataMsgConstructor entityDataMsgConstructor; @MockBean - protected RuleChainMsgConstructor ruleChainMsgConstructor; + protected AdminSettingsMsgConstructorV1 adminSettingsMsgConstructorV1; @MockBean - protected AlarmMsgConstructor alarmMsgConstructor; + protected AdminSettingsMsgConstructorV2 adminSettingsMsgConstructorV2; + + @MockBean + protected AlarmMsgConstructorV1 alarmMsgConstructorV1; + + @MockBean + protected AlarmMsgConstructorV2 alarmMsgConstructorV2; + + @SpyBean + protected AssetMsgConstructorV1 assetMsgConstructorV1; + + @SpyBean + protected AssetMsgConstructorV2 assetMsgConstructorV2; + + @MockBean + protected CustomerMsgConstructorV1 customerMsgConstructorV1; + + @MockBean + protected CustomerMsgConstructorV2 customerMsgConstructorV2; + + @MockBean + protected DashboardMsgConstructorV1 dashboardMsgConstructorV1; + + @MockBean + protected DashboardMsgConstructorV2 dashboardMsgConstructorV2; + + @SpyBean + protected DeviceMsgConstructorV1 deviceMsgConstructorV1; + + @SpyBean + protected DeviceMsgConstructorV2 deviceMsgConstructorV2; + + @MockBean + protected EntityViewMsgConstructorV1 entityViewMsgConstructorV1; + + @MockBean + protected EntityViewMsgConstructorV2 entityViewMsgConstructorV2; + + @MockBean + protected OtaPackageMsgConstructorV1 otaPackageMsgConstructorV1; + + @MockBean + protected OtaPackageMsgConstructorV2 otaPackageMsgConstructorV2; + + @MockBean + protected QueueMsgConstructorV1 queueMsgConstructorV1; + + @MockBean + protected QueueMsgConstructorV2 queueMsgConstructorV2; + + @MockBean + protected RelationMsgConstructorV1 relationMsgConstructorV1; + + @MockBean + protected RelationMsgConstructorV2 relationMsgConstructorV2; + + @MockBean + protected ResourceMsgConstructorV1 resourceMsgConstructorV1; + + @MockBean + protected ResourceMsgConstructorV2 resourceMsgConstructorV2; @SpyBean - protected DeviceMsgConstructor deviceMsgConstructor; + protected RuleChainMsgConstructorV1 ruleChainMsgConstructorV1; @SpyBean - protected AssetMsgConstructor assetMsgConstructor; + protected RuleChainMsgConstructorV2 ruleChainMsgConstructorV2; + + @MockBean + protected TenantMsgConstructorV1 tenantMsgConstructorV1; @MockBean - protected EntityViewMsgConstructor entityViewMsgConstructor; + protected TenantMsgConstructorV2 tenantMsgConstructorV2; @MockBean - protected DashboardMsgConstructor dashboardMsgConstructor; + protected UserMsgConstructorV1 userMsgConstructorV1; @MockBean - protected RelationMsgConstructor relationMsgConstructor; + protected UserMsgConstructorV2 userMsgConstructorV2; @MockBean - protected UserMsgConstructor userMsgConstructor; + protected WidgetMsgConstructorV1 widgetMsgConstructorV1; + + @MockBean + protected WidgetMsgConstructorV2 widgetMsgConstructorV2; + + @SpyBean + protected RuleChainMsgConstructorFactory ruleChainMsgConstructorFactory; @MockBean - protected CustomerMsgConstructor customerMsgConstructor; + protected AlarmMsgConstructorFactory alarmMsgConstructorFactory; @SpyBean - protected DeviceProfileMsgConstructor deviceProfileMsgConstructor; + protected DeviceMsgConstructorFactory deviceMsgConstructorFactory; @SpyBean - protected AssetProfileMsgConstructor assetProfileMsgConstructor; + protected AssetMsgConstructorFactory assetMsgConstructorFactory; + + @MockBean + protected DashboardMsgConstructorFactory dashboardMsgConstructorFactory; + + @MockBean + protected EntityViewMsgConstructorFactory entityViewMsgConstructorFactory; + + @MockBean + protected RelationMsgConstructorFactory relationMsgConstructorFactory; @MockBean - protected TenantMsgConstructor tenantMsgConstructor; + protected UserMsgConstructorFactory userMsgConstructorFactory; @MockBean - protected TenantProfileMsgConstructor tenantProfileMsgConstructor; + protected CustomerMsgConstructorFactory customerMsgConstructorFactory; @MockBean - protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; + protected TenantMsgConstructorFactory tenantMsgConstructorFactory; @MockBean - protected WidgetTypeMsgConstructor widgetTypeMsgConstructor; + protected WidgetMsgConstructorFactory widgetBundleMsgConstructorFactory; @MockBean - protected AdminSettingsMsgConstructor adminSettingsMsgConstructor; + protected AdminSettingsMsgConstructorFactory adminSettingsMsgConstructorFactory; @MockBean - protected OtaPackageMsgConstructor otaPackageMsgConstructor; + protected OtaPackageMsgConstructorFactory otaPackageMsgConstructorFactory; @MockBean - protected QueueMsgConstructor queueMsgConstructor; + protected QueueMsgConstructorFactory queueMsgConstructorFactory; @MockBean - protected ResourceMsgConstructor resourceMsgConstructor; + protected ResourceMsgConstructorFactory resourceMsgConstructorFactory; @MockBean protected EdgeSynchronizationManager edgeSynchronizationManager; diff --git a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java index 26eead0ec7..de8fc45381 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.service.install.update.ImagesUpdater; import java.io.IOException; import java.nio.file.Path; @@ -63,6 +64,8 @@ class InstallScriptsTest { OAuth2ConfigTemplateService oAuth2TemplateService; @MockBean ResourceService resourceService; + @MockBean + ImagesUpdater imagesUpdater; @SpyBean InstallScripts installScripts; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 26be4e9bd3..ba0b4e4763 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -113,15 +113,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testSubscribingToUnreadNotificationsCount() { + wsClient.subscribeForUnreadNotificationsCount().waitForReply(true); NotificationTarget notificationTarget = createNotificationTarget(customerUserId); String notificationText1 = "Notification 1"; submitNotificationRequest(notificationTarget.getId(), notificationText1); String notificationText2 = "Notification 2"; submitNotificationRequest(notificationTarget.getId(), notificationText2); - wsClient.subscribeForUnreadNotificationsCount(); - wsClient.waitForReply(true); - await().atMost(2, TimeUnit.SECONDS) .until(() -> wsClient.getLastCountUpdate().getTotalUnreadCount() == 2); } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index 6f08e47412..278420589f 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.resource.TbResourceService; @@ -46,6 +48,7 @@ import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -105,14 +108,17 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { ""; private static final String DEFAULT_FILE_NAME = "test.jks"; - private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; + private static final String TEST_BASE64_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; + private static final byte[] TEST_DATA = Base64.getDecoder().decode(TEST_BASE64_DATA); private IdComparator idComparator = new IdComparator<>(); private TenantId tenantId; @Autowired - private TbResourceService resourceService; + private ResourceService resourceService; + @Autowired + private TbResourceService tbResourceService; private Tenant savedTenant; private User tenantAdmin; @@ -151,7 +157,8 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { long limit = 4; EntityInfo defaultTenantProfileInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class); TenantProfile defaultTenantProfile = doGet("/api/tenantProfile/" + defaultTenantProfileInfo.getId().getId().toString(), TenantProfile.class); - defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(limit).build()); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder() + .maxResourcesInBytes(limit).build()); doPost("/api/tenantProfile", defaultTenantProfile, TenantProfile.class); loginTenantAdmin(); @@ -165,7 +172,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { try { assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME)) .isInstanceOf(DataValidationException.class) - .hasMessageContaining("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit); + .hasMessageContaining("Resources total size exceeds the maximum of %s bytes", limit); } finally { defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(0).build()); loginSysAdmin(); @@ -173,6 +180,15 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { } } + @Test + public void testMaxResourceSizeValidation() throws ThingsboardException { + updateDefaultTenantProfileConfig(profileConfig -> { + profileConfig.setMaxResourceSize(2); + }); + assertThatThrownBy(() -> createResource("Test", DEFAULT_FILE_NAME)) + .hasMessageContaining("Resource exceeds the maximum size of 2 bytes"); + } + @Test public void sumDataSizeByTenantId() throws Exception { assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); @@ -182,7 +198,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { for (int i = 2; i < 4; i++) { createResource("test" + i, i + DEFAULT_FILE_NAME); - assertEquals(i*4, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(i * 4, resourceService.sumDataSizeByTenantId(tenantId)); } } @@ -192,9 +208,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle(title); resource.setResourceType(ResourceType.JKS); resource.setFileName(filename); - byte[] b = new byte[1]; - resource.setData(Base64.getEncoder().encodeToString(b)); - return resourceService.save(resource); + byte[] b = new byte[]{1, 2, 3, 4}; + resource.setData(b); + return tbResourceService.save(resource); } @Test @@ -206,7 +222,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); Assert.assertNotNull(savedResource); Assert.assertNotNull(savedResource.getId()); @@ -214,15 +230,17 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(resource.getTenantId(), savedResource.getTenantId()); assertEquals(resource.getTitle(), savedResource.getTitle()); assertEquals(resource.getResourceKey(), savedResource.getResourceKey()); - assertEquals(resource.getData(), savedResource.getData()); + assertArrayEquals(TEST_DATA, savedResource.getData()); savedResource.setTitle("My new resource"); + savedResource.setData(null); - resourceService.save(savedResource); + tbResourceService.save(savedResource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); assertEquals(foundResource.getTitle(), savedResource.getTitle()); + assertArrayEquals(foundResource.getData(), TEST_DATA); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } @Test @@ -231,9 +249,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTenantId(tenantId); resource.setResourceType(ResourceType.LWM2M_MODEL); resource.setFileName("test_model.xml"); - resource.setData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL.getBytes())); + resource.setEncodedData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL.getBytes())); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); Assert.assertNotNull(savedResource); Assert.assertNotNull(savedResource.getId()); @@ -241,9 +259,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(resource.getTenantId(), savedResource.getTenantId()); assertEquals("My first resource id=0 v1.0", savedResource.getTitle()); assertEquals("0_1.0", savedResource.getResourceKey()); - assertEquals(resource.getData(), savedResource.getData()); + assertArrayEquals(savedResource.getData(), LWM2M_TEST_MODEL.getBytes()); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } @Test @@ -253,11 +271,11 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } @Test @@ -269,7 +287,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); TbResource resource2 = new TbResource(); resource.setTenantId(tenantId); @@ -280,10 +298,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { try { Assertions.assertThrows(DataValidationException.class, () -> { - resourceService.save(resource2); + tbResourceService.save(resource2); }); } finally { - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } } @@ -295,7 +313,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); Assertions.assertThrows(DataValidationException.class, () -> { - resourceService.save(resource); + tbResourceService.save(resource); }); } @@ -308,7 +326,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); Assertions.assertThrows(DataValidationException.class, () -> { - resourceService.save(resource); + tbResourceService.save(resource); }); } @@ -318,10 +336,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTenantId(tenantId); resource.setResourceType(ResourceType.LWM2M_MODEL); resource.setFileName("xxe_test_model.xml"); - resource.setData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL_WITH_XXE.getBytes())); + resource.setData(LWM2M_TEST_MODEL_WITH_XXE.getBytes()); DataValidationException thrown = assertThrows(DataValidationException.class, () -> { - resourceService.save(resource); + tbResourceService.save(resource); }); assertEquals("Failed to parse file xxe_test_model.xml", thrown.getMessage()); } @@ -334,12 +352,12 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); Assert.assertNotNull(foundResource); assertEquals(savedResource, foundResource); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } @Test @@ -350,12 +368,12 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); - TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey()); + TbResource foundResource = resourceService.findResourceByTenantIdAndKey(tenantId, savedResource.getResourceType(), savedResource.getResourceKey()); Assert.assertNotNull(foundResource); assertEquals(savedResource, foundResource); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); } @Test @@ -365,11 +383,11 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResource savedResource = resourceService.save(resource); + TbResource savedResource = tbResourceService.save(resource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); Assert.assertNotNull(foundResource); - resourceService.delete(savedResource, null); + tbResourceService.delete(savedResource, null); foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); Assert.assertNull(foundResource); } @@ -391,7 +409,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - resources.add(new TbResourceInfo(resourceService.save(resource))); + resources.add(new TbResourceInfo(tbResourceService.save(resource))); } List loadedResources = new ArrayList<>(); @@ -445,7 +463,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - TbResourceInfo tbResourceInfo = new TbResourceInfo(resourceService.save(resource)); + TbResourceInfo tbResourceInfo = new TbResourceInfo(tbResourceService.save(resource)); if (i >= 50) { resources.add(tbResourceInfo); } @@ -458,7 +476,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData(TEST_DATA); - resources.add(new TbResourceInfo(resourceService.save(resource))); + resources.add(new TbResourceInfo(tbResourceService.save(resource))); } List loadedResources = new ArrayList<>(); @@ -468,7 +486,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { TbResourceInfoFilter filter = TbResourceInfoFilter.builder() .tenantId(tenantId) .build(); - pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink); + pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink); loadedResources.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java index 33c11ea91d..43d8960f3b 100644 --- a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -44,13 +44,13 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; +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.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.resource.TbResourceService; import java.io.IOException; import java.nio.file.Files; @@ -96,7 +96,7 @@ public class DefaultTransportApiServiceTest { @MockBean protected DeviceProvisionService deviceProvisionService; @MockBean - protected TbResourceService resourceService; + protected ResourceService resourceService; @MockBean protected OtaPackageService otaPackageService; @MockBean diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java index 6c5f682a4f..3124571dc8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.coap; +import lombok.Getter; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapObserveRelation; @@ -34,7 +35,10 @@ public class CoapTestClient { private final CoapClient client; - public CoapTestClient(){ + @Getter + private CoAP.Type type = CoAP.Type.CON; + + public CoapTestClient() { this.client = createClient(); } @@ -80,9 +84,13 @@ public class CoapTestClient { return client.setTimeout(CLIENT_REQUEST_TIMEOUT).get(); } - public CoapObserveRelation getObserveRelation(CoapTestCallback callback){ + public CoapObserveRelation getObserveRelation(CoapTestCallback callback) { + return getObserveRelation(callback, true); + } + + public CoapObserveRelation getObserveRelation(CoapTestCallback callback, boolean confirmable) { Request request = Request.newGet().setObserve(); - request.setType(CoAP.Type.CON); + request.setType(confirmable ? CoAP.Type.CON : CoAP.Type.NON); return client.observe(request, callback); } @@ -94,12 +102,28 @@ public class CoapTestClient { } public void setURI(String accessToken, FeatureType featureType) { - if (featureType == null){ + if (featureType == null) { featureType = FeatureType.ATTRIBUTES; } setURI(getFeatureTokenUrl(accessToken, featureType)); } + public void useCONs() { + if (client == null) { + throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); + } + type = CoAP.Type.CON; + client.useCONs(); + } + + public void useNONs() { + if (client == null) { + throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); + } + type = CoAP.Type.NON; + client.useNONs(); + } + private CoapClient createClient() { return new CoapClient(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java new file mode 100644 index 0000000000..e50812403b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -0,0 +1,309 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.coap.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.msg.session.FeatureType; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest; +import org.thingsboard.server.transport.coap.CoapTestCallback; +import org.thingsboard.server.transport.coap.CoapTestClient; +import org.thingsboard.server.transport.coap.CoapTestConfigProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertArrayEquals; +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; +import static org.thingsboard.server.common.data.query.EntityKeyType.CLIENT_ATTRIBUTE; +import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTRIBUTE; + +@Slf4j +@DaoSqlTest +public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { + + private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + private static final List EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; + + + @Before + public void beforeTest() throws Exception { + CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() + .deviceName("Test Post Attributes device") + .build(); + processBeforeTest(configProperties); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testConfirmableRequests() throws Exception { + boolean confirmable = true; + processAttributesTest(confirmable); + processTwoWayRpcTest(confirmable); + processTestRequestAttributesValuesFromTheServer(confirmable); + } + + @Test + public void testNonConfirmableRequests() throws Exception { + boolean confirmable = false; + processAttributesTest(confirmable); + processTwoWayRpcTest(confirmable); + processTestRequestAttributesValuesFromTheServer(confirmable); + } + + protected void processAttributesTest(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); + CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR.getBytes()); + assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); + assertEquals("CoAP response type is wrong!", client.getType(), coapResponse.advanced().getType()); + + DeviceId deviceId = savedDevice.getId(); + List actualKeys = getActualKeysList(deviceId); + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + Set expectedKeySet = new HashSet<>(EXPECTED_KEYS); + assertEquals(expectedKeySet, actualKeySet); + + String attributesValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/attributes/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + ; + List> values = doGetAsyncTyped(attributesValuesUrl, new TypeReference<>() { + }); + assertAttributesValues(values, actualKeySet); + String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + doDelete(deleteAttributesUrl); + } + + protected void processTwoWayRpcTest(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.RPC, confirmable); + CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client); + + CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap, confirmable); + String awaitAlias = "await Two Way Rpc (client.getObserveRelation)"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + 0 == callbackCoap.getObserve()); + validateCurrentStateNotification(callbackCoap); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve() + 1; + String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve()); + validateTwoWayStateChangedNotification(callbackCoap, actualResult); + + int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve() + 1; + actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + awaitAlias = "await Two Way Rpc (setGpio(method, params, value) second"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve()); + + validateTwoWayStateChangedNotification(callbackCoap, actualResult); + + observeRelation.proactiveCancel(); + assertTrue(observeRelation.isCanceled()); + } + + protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); + SingleEntityFilter dtf = new SingleEntityFilter(); + dtf.setSingleEntity(savedDevice.getId()); + List csKeys = getEntityKeys(CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + getWsClient().subscribeLatestUpdate(keys, dtf); + getWsClient().registerWaitForUpdate(2); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", + PAYLOAD_VALUES_STR, String.class, status().isOk()); + + CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR); + assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); + + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + + String keysParam = String.join(",", EXPECTED_KEYS); + String featureTokenUrl = CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.ATTRIBUTES) + "?clientKeys=" + keysParam + "&sharedKeys=" + keysParam; + client.setURI(featureTokenUrl); + CoapResponse response = client.getMethod(); + assertEquals("CoAP response type is wrong!", client.getType(), response.advanced().getType()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void assertAttributesValues(List> deviceValues, Set keySet) { + for (Map map : deviceValues) { + String key = (String) map.get("key"); + Object value = map.get("value"); + assertTrue(keySet.contains(key)); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals(true, value); + break; + case "key3": + assertEquals(3.0, value); + break; + case "key4": + assertEquals(4, value); + break; + case "key5": + assertNotNull(value); + assertEquals(3, ((LinkedHashMap) value).size()); + assertEquals(42, ((LinkedHashMap) value).get("someNumber")); + assertEquals(Arrays.asList(1, 2, 3), ((LinkedHashMap) value).get("someArray")); + LinkedHashMap someNestedObject = (LinkedHashMap) ((LinkedHashMap) value).get("someNestedObject"); + assertEquals("value", someNestedObject.get("key")); + break; + } + } + } + + private List getActualKeysList(DeviceId deviceId) throws Exception { + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + }); + if (actualKeys.size() == EXPECTED_KEYS.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + return actualKeys; + } + + private void validateCurrentStateNotification(CoapTestCallback callback) { + assertArrayEquals(EMPTY_PAYLOAD, callback.getPayloadBytes()); + } + + private void validateTwoWayStateChangedNotification(CoapTestCallback callback, String actualResult) { + assertEquals(DEVICE_RESPONSE, actualResult); + assertNotNull(callback.getPayloadBytes()); + } + + protected class TestCoapCallbackForRPC extends CoapTestCallback { + + private final CoapTestClient client; + + @Getter + private boolean wasSuccessful = false; + + TestCoapCallbackForRPC(CoapTestClient client) { + this.client = client; + } + + @Override + public void onLoad(CoapResponse response) { + payloadBytes = response.getPayload(); + responseCode = response.getCode(); + observe = response.getOptions().getObserve(); + wasSuccessful = client.getType().equals(response.advanced().getType()); + if (observe != null) { + if (observe > 0) { + processOnLoadResponse(response, client); + } + } + } + + @Override + public void onError() { + log.warn("Command Response Ack Error, No connect"); + } + } + + protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { + JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload()); + int requestId = responseJson.get("id").asInt(); + client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId)); + client.postMethod(new CoapHandler() { + @Override + public void onLoad(CoapResponse response) { + log.warn("RPC {} command response ack: {}", requestId, response.getCode()); + } + + @Override + public void onError() { + log.warn("RPC {} command response ack error, no connect", requestId); + } + }, DEVICE_RESPONSE, MediaTypeRegistry.APPLICATION_JSON); + } + + private CoapTestClient createClientForFeatureWithConfirmableParameter(FeatureType featureType, boolean confirmable) { + CoapTestClient coapTestClient = new CoapTestClient(accessToken, featureType); + if (confirmable) { + coapTestClient.useCONs(); + } else { + coapTestClient.useNONs(); + } + return coapTestClient; + } + + private List getEntityKeys(EntityKeyType scope) { + return CoapClientIntegrationTest.EXPECTED_KEYS.stream().map(key -> new EntityKey(scope, key)).collect(Collectors.toList()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 64382d5d51..00c6ba6a09 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -206,7 +206,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte lwModel.setFileName(resourceName); lwModel.setTenantId(tenantId); byte[] bytes = IOUtils.toByteArray(AbstractLwM2MIntegrationTest.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName)); - lwModel.setData(Base64.getEncoder().encodeToString(bytes)); + lwModel.setData(bytes); lwModel = doPostWithTypedResponse("/api/resource", lwModel, new TypeReference<>() { }); Assert.assertNotNull(lwModel); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index c5af954392..01a18ad7ad 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -17,6 +17,7 @@ package org.thingsboard.server.cache; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.support.NullValue; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -27,6 +28,7 @@ import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.thingsboard.server.common.data.FstStatsService; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.util.JedisClusterCRC16; @@ -44,6 +46,9 @@ public abstract class RedisTbTransactionalCache newTransactionForKeys(List keys); + default V getOrFetchFromDB(K key, Supplier dbCall, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheNullValue); + } else { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + return cacheValueWrapper.get(); + } + return dbCall.get(); + } + } + default V getAndPutInTransaction(K key, Supplier dbCall, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { @@ -69,6 +81,19 @@ public interface TbTransactionalCache R getOrFetchFromDB(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); + } else { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + var cacheValue = cacheValueWrapper.get(); + return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); + } + return dbCall.get(); + } + } + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 6a0831de47..cd8f551aa7 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -40,6 +40,10 @@ org.thingsboard.common data + + org.thingsboard.common + proto + org.thingsboard.common message diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 5b1eb1ae4f..5c3b9076ff 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -83,7 +83,9 @@ public interface TbClusterService extends TbQueueClusterService { void onDeviceUpdated(Device device, Device old); - void onDeviceDeleted(Device device, TbQueueCallback callback); + void onDeviceDeleted(TenantId tenantId, Device device, TbQueueCallback callback); + + void onDeviceAssignedToTenant(TenantId oldTenantId, Device device); void onResourceChange(TbResource resource, TbQueueCallback callback); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index 1ec6fb3fb5..477c352b54 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -27,8 +27,12 @@ public interface AssetProfileService extends EntityDaoService { AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId); + AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId, boolean putInCache); + AssetProfile findAssetProfileByName(TenantId tenantId, String profileName); + AssetProfile findAssetProfileByName(TenantId tenantId, String profileName, boolean putInCache); + AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId); AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 434088180c..d94256bb99 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -29,7 +29,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; public interface DashboardService extends EntityDaoService { - + Dashboard findDashboardById(TenantId tenantId, DashboardId dashboardId); ListenableFuture findDashboardByIdAsync(TenantId tenantId, DashboardId dashboardId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index f94b709c97..5d325aa1a6 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -27,8 +27,12 @@ public interface DeviceProfileService extends EntityDaoService { DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId); + DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId, boolean putInCache); + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName); + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName, boolean putInCache); + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 09bad2b48d..7a020767f5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -50,6 +50,8 @@ public interface EntityViewService extends EntityDaoService { EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId); + EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache); + EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name); PageData findEntityViewByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java new file mode 100644 index 0000000000..f51f83d2f0 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.resource; + +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.TbImageDeleteResult; +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 org.thingsboard.server.common.data.widget.WidgetTypeDetails; + +public interface ImageService { + + TbResourceInfo saveImage(TbResource image); + + TbResourceInfo saveImageInfo(TbResourceInfo imageInfo); + + TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key); + + PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink); + + byte[] getImageData(TenantId tenantId, TbResourceId imageId); + + byte[] getImagePreview(TenantId tenantId, TbResourceId imageId); + + TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force); + + TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag); + + boolean replaceBase64WithImageUrl(HasImage entity, String type); + boolean replaceBase64WithImageUrl(Dashboard dashboard); + boolean replaceBase64WithImageUrl(WidgetTypeDetails widgetType); + + void inlineImage(HasImage entity); + + void inlineImages(Dashboard dashboard); + + void inlineImages(WidgetTypeDetails widgetTypeDetails); +} 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 index 5bc45ebc29..b4cda2d733 100644 --- 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 @@ -34,12 +34,14 @@ public interface ResourceService extends EntityDaoService { TbResource saveResource(TbResource resource, boolean doValidate); - TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); + TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + PageData findAllTenantResources(TenantId tenantId, PageLink pageLink); ListenableFuture findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId); @@ -54,7 +56,10 @@ public interface ResourceService extends EntityDaoService { void deleteResource(TenantId tenantId, TbResourceId resourceId); + void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); + void deleteResourcesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index fe08934f79..19b69f4f27 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 @@ -30,7 +30,9 @@ import java.util.Objects; import java.util.Set; @ApiModel -public class DashboardInfo extends BaseData implements HasName, HasTenantId, HasTitle { +public class DashboardInfo extends BaseData implements HasName, HasTenantId, HasTitle, HasImage { + + private static final long serialVersionUID = -9080404114760433799L; private TenantId tenantId; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 544f5805a6..ca48ef364a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -49,6 +49,8 @@ public class DataConstants { public static final String MQTT_TRANSPORT_NAME = "MQTT"; public static final String HTTP_TRANSPORT_NAME = "HTTP"; public static final String SNMP_TRANSPORT_NAME = "SNMP"; + public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!"; + public static final String TB_IMAGE_PREFIX = "tb-image;"; public static final String[] allScopes() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 196edadf9c..abee7284c8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -41,7 +41,7 @@ import java.io.IOException; @ToString(exclude = {"image", "profileDataBytes"}) @EqualsAndHashCode(callSuper = true) @Slf4j -public class DeviceProfile extends BaseData implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity { +public class DeviceProfile extends BaseData implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity, HasImage { private static final long serialVersionUID = 6998485460273302018L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FstStatsService.java b/common/data/src/main/java/org/thingsboard/server/common/data/FstStatsService.java new file mode 100644 index 0000000000..42be4b3b68 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/FstStatsService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface FstStatsService { + + void incrementEncode(Class clazz); + + void incrementDecode(Class clazz); + + void recordEncodeTime(Class clazz, long startTime); + + void recordDecodeTime(Class clazz, long startTime); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasImage.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasImage.java new file mode 100644 index 0000000000..38b01e1005 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasImage.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasImage extends HasTenantId, HasName { + + String getImage(); + + void setImage(String image); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java new file mode 100644 index 0000000000..367531565f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ImageDescriptor { + private String mediaType; + private int width; + private int height; + private long size; + private String etag; + private ImageDescriptor previewDescriptor; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java new file mode 100644 index 0000000000..d1b33f202a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@ApiModel +@Slf4j +@Data +public class ImageExportData { + + private final String mediaType; + private final String fileName; + private final String title; + private final String resourceKey; + private final String data; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java index 3c2b9a4bc0..22af597627 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java @@ -16,20 +16,21 @@ package org.thingsboard.server.common.data; import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor public enum ResourceType { - LWM2M_MODEL("application/xml", false), - JKS("application/x-java-keystore", false), - PKCS_12("application/x-pkcs12", false), - JS_MODULE("application/javascript", true); + LWM2M_MODEL("application/xml", false, false), + JKS("application/x-java-keystore", false, false), + PKCS_12("application/x-pkcs12", false, false), + JS_MODULE("application/javascript", true, true), + IMAGE(null, true, true); @Getter private final String mediaType; @Getter private final boolean customerAccess; + @Getter + private final boolean updatable; - ResourceType(String mediaType, boolean customerAccess) { - this.mediaType = mediaType; - this.customerAccess = customerAccess; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 3abbde8401..5a787f24a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -173,6 +173,10 @@ public class StringUtils { return false; } + public static String substringBeforeLast(String str, String separator) { + return org.apache.commons.lang3.StringUtils.substringBeforeLast(str, separator); + } + public static String substringAfterLast(String str, String sep) { return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbImageDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbImageDeleteResult.java new file mode 100644 index 0000000000..9782b2b58f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbImageDeleteResult.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.HasId; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class TbImageDeleteResult { + + private boolean success; + private Map>> references; + +} 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 8b82db196a..2324eae1a7 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 @@ -15,13 +15,16 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.annotations.ApiModelProperty; 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.Length; -import org.thingsboard.server.common.data.validation.NoXss; + +import java.util.Base64; +import java.util.Optional; @Slf4j @Data @@ -30,13 +33,9 @@ public class TbResource extends TbResourceInfo { private static final long serialVersionUID = 7379609705527272306L; - @NoXss - @Length(fieldName = "file name") - @ApiModelProperty(position = 8, value = "Resource file name.", example = "19.xml", accessMode = ApiModelProperty.AccessMode.READ_ONLY) - private String fileName; + private byte[] data; - @ApiModelProperty(position = 9, value = "Resource data.", example = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo=", accessMode = ApiModelProperty.AccessMode.READ_ONLY) - private String data; + private byte[] preview; public TbResource() { super(); @@ -52,31 +51,41 @@ public class TbResource extends TbResourceInfo { public TbResource(TbResource resource) { super(resource); - this.data = resource.getData(); + this.data = resource.data; + this.preview = resource.preview; + } + + @ApiModelProperty(position = 12, value = "Resource data.", example = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo=", accessMode = ApiModelProperty.AccessMode.READ_WRITE) + @JsonGetter("data") + public String getEncodedData() { + return Optional.ofNullable(data) + .map(Base64.getEncoder()::encodeToString) + .orElse(null); + } + + @JsonSetter("data") + public void setEncodedData(String data) { + this.data = Optional.ofNullable(data) + .map(Base64.getDecoder()::decode) + .orElse(null); + } + + @JsonGetter("preview") + public String getEncodedPreview() { + return Optional.ofNullable(preview) + .map(Base64.getEncoder()::encodeToString) + .orElse(null); + } + + @JsonSetter("preview") + public void setEncodedPreview(String preview) { + this.preview = Optional.ofNullable(preview) + .map(Base64.getDecoder()::decode) + .orElse(null); } @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("Resource [tenantId="); - builder.append(getTenantId()); - builder.append(", id="); - builder.append(getUuidId()); - builder.append(", createdTime="); - builder.append(createdTime); - builder.append(", title="); - builder.append(getTitle()); - builder.append(", resourceType="); - builder.append(getResourceType()); - builder.append(", resourceKey="); - builder.append(getResourceKey()); - builder.append(", fileName="); - builder.append(fileName); - builder.append(", data="); - builder.append(data); - builder.append(", hashCode="); - builder.append(getEtag()); - builder.append("]"); - return builder.toString(); + return super.toString(); } } 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 af3c71f226..b8d2267663 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 @@ -16,6 +16,9 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -26,11 +29,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.function.UnaryOperator; + @ApiModel @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class TbResourceInfo extends BaseData implements HasName, HasTenantId { +public class TbResourceInfo extends BaseData implements HasName, HasTenantId, ExportableEntity { private static final long serialVersionUID = 7282664529021651736L; @@ -48,8 +53,16 @@ public class TbResourceInfo extends BaseData implements HasName, H private String resourceKey; @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String searchText; + @ApiModelProperty(position = 8, value = "Resource etag.", example = "33a64df551425fcc55e4d42a148795d9f25f89d4", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String etag; + @NoXss + @Length(fieldName = "file name") + @ApiModelProperty(position = 9, value = "Resource file name.", example = "19.xml", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String fileName; + private JsonNode descriptor; + + private TbResourceId externalId; public TbResourceInfo() { super(); @@ -61,18 +74,21 @@ public class TbResourceInfo extends BaseData implements HasName, H public TbResourceInfo(TbResourceInfo resourceInfo) { super(resourceInfo); - this.tenantId = resourceInfo.getTenantId(); - this.title = resourceInfo.getTitle(); - this.resourceType = resourceInfo.getResourceType(); - this.resourceKey = resourceInfo.getResourceKey(); - this.searchText = resourceInfo.getSearchText(); - this.etag = resourceInfo.getEtag(); + this.tenantId = resourceInfo.tenantId; + this.title = resourceInfo.title; + this.resourceType = resourceInfo.resourceType; + this.resourceKey = resourceInfo.resourceKey; + this.searchText = resourceInfo.searchText; + this.etag = resourceInfo.etag; + this.fileName = resourceInfo.fileName; + this.descriptor = resourceInfo.descriptor != null ? resourceInfo.descriptor.deepCopy() : null; + this.externalId = resourceInfo.externalId; } @ApiModelProperty(position = 1, value = "JSON object with the Resource Id. " + "Specify this field to update the Resource. " + "Referencing non-existing Resource Id will cause error. " + - "Omit this field to create new Resource." ) + "Omit this field to create new Resource.") @Override public TbResourceId getId() { return super.getId(); @@ -85,34 +101,37 @@ public class TbResourceInfo extends BaseData implements HasName, H } @Override - @JsonIgnore + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getLink() { + if (resourceType == ResourceType.IMAGE) { + return "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git + } + return null; + } + @JsonIgnore public String getSearchText() { - return searchText != null ? searchText : title; + return title; } - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("ResourceInfo [tenantId="); - builder.append(tenantId); - builder.append(", id="); - builder.append(getUuidId()); - builder.append(", createdTime="); - builder.append(createdTime); - builder.append(", title="); - builder.append(title); - builder.append(", resourceType="); - builder.append(resourceType); - builder.append(", resourceKey="); - builder.append(resourceKey); - builder.append(", hashCode="); - builder.append(etag); - builder.append("]"); - return builder.toString(); + public T getDescriptor(Class type) throws JsonProcessingException { + return descriptor != null ? mapper.treeToValue(descriptor, type) : null; + } + + public void updateDescriptor(Class type, UnaryOperator updater) throws JsonProcessingException { + T descriptor = getDescriptor(type); + descriptor = updater.apply(descriptor); + setDescriptorValue(descriptor); } + + @JsonIgnore + public void setDescriptorValue(Object value) { + this.descriptor = value != null ? mapper.valueToTree(value) : null; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java index 9057fc144b..5c0747f100 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java @@ -19,11 +19,13 @@ import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Set; + @Data @Builder public class TbResourceInfoFilter { private TenantId tenantId; - private ResourceType resourceType; + private Set resourceTypes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java index 5456ba36ce..96b24ad2f2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java @@ -22,8 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasTenantId; @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ToString(exclude = {"image"}) @EqualsAndHashCode(callSuper = true) @Slf4j -public class AssetProfile extends BaseData implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity { +public class AssetProfile extends BaseData implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity, HasImage { private static final long serialVersionUID = 6998485460273302018L; @@ -114,7 +114,7 @@ public class AssetProfile extends BaseData implements HasName, H } @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the asset profile is not specified during asset creation.") - public boolean isDefault(){ + public boolean isDefault() { return isDefault; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java index 3688f5c6c2..1baf707040 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java @@ -31,6 +31,8 @@ import java.util.UUID; @ToString(callSuper = true) public class EdgeEvent extends BaseData { + private static final long serialVersionUID = 5548866356798094088L; + private long seqId; private TenantId tenantId; private EdgeId edgeId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java index 543ac1ba6f..d0a29695c0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.common.data.id; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.UUID; public class UserCredentialsId extends UUIDBased { - public UserCredentialsId(UUID id){ + @JsonCreator + public UserCredentialsId(@JsonProperty("id")UUID id){ super(id); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 92c6a332df..10f20cc973 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.notification.rule.NotificationRule; @@ -56,7 +57,8 @@ import java.lang.annotation.Target; @Type(name = "WIDGET_TYPE", value = WidgetTypeDetails.class), @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), - @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class) + @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), + @Type(name = "TB_RESOURCE", value = TbResource.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) public @interface JsonTbEntity { 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 a1d6da34d8..dfc6f5206b 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 @@ -39,6 +39,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxRuleChains; private long maxResourcesInBytes; private long maxOtaPackagesInBytes; + private long maxResourceSize; private String transportTenantMsgRateLimit; private String transportTenantTelemetryMsgRateLimit; 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 424e0bc6bc..b335038b54 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 @@ -21,6 +21,7 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -29,10 +30,10 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" }) -public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, ExportableEntity { +public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Length(fieldName = "image", max = 1000000) - @ApiModelProperty(position = 9, value = "Base64 encoded thumbnail") + @ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") private String image; @NoXss @Length(fieldName = "description", max = 1024) 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 9096207abe..ce0367f57d 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 @@ -22,8 +22,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTitle; @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class WidgetsBundle extends BaseData implements HasName, HasTenantId, ExportableEntity, HasTitle { +public class WidgetsBundle extends BaseData implements HasName, HasTenantId, ExportableEntity, HasTitle, HasImage { private static final long serialVersionUID = -7627368878362410489L; @@ -60,12 +60,13 @@ public class WidgetsBundle extends BaseData implements HasName, @Length(fieldName = "image", max = 1000000) @Getter @Setter - @ApiModelProperty(position = 6, value = "Base64 encoded thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 6, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String image; @NoXss @Length(fieldName = "description", max = 1024) @Getter + @Setter @ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String description; diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index a9b12865a2..973c50c1d2 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_6_1) + .setEdgeVersion(EdgeVersion.V_3_6_2) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 71e84304ed..18b30e439f 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -36,6 +36,7 @@ enum EdgeVersion { V_3_4_0 = 2; V_3_6_0 = 3; V_3_6_1 = 4; + V_3_6_2 = 5; } /** @@ -138,25 +139,28 @@ message RuleChainUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string name = 4; - optional int64 firstRuleNodeIdMSB = 5; - optional int64 firstRuleNodeIdLSB = 6; - bool root = 7; - bool debugMode = 8; - string configuration = 9; + string name = 4 [deprecated = true]; + optional int64 firstRuleNodeIdMSB = 5 [deprecated = true]; + optional int64 firstRuleNodeIdLSB = 6 [deprecated = true]; + bool root = 7 [deprecated = true]; + bool debugMode = 8 [deprecated = true]; + string configuration = 9 [deprecated = true]; + string entity = 10; } message RuleChainMetadataUpdateMsg { UpdateMsgType msgType = 1; - int64 ruleChainIdMSB = 2; - int64 ruleChainIdLSB = 3; - int32 firstNodeIndex = 4; - repeated RuleNodeProto nodes = 5; - repeated NodeConnectionInfoProto connections = 6; - repeated RuleChainConnectionInfoProto ruleChainConnections = 7; + int64 ruleChainIdMSB = 2 [deprecated = true]; + int64 ruleChainIdLSB = 3 [deprecated = true]; + int32 firstNodeIndex = 4 [deprecated = true]; + repeated RuleNodeProto nodes = 5 [deprecated = true]; + repeated NodeConnectionInfoProto connections = 6 [deprecated = true]; + repeated RuleChainConnectionInfoProto ruleChainConnections = 7 [deprecated = true]; + string entity = 8; } message RuleNodeProto { + option deprecated = true; int64 idMSB = 1; int64 idLSB = 2; string type = 3; @@ -169,12 +173,14 @@ message RuleNodeProto { } message NodeConnectionInfoProto { + option deprecated = true; int32 fromIndex = 1; int32 toIndex = 2; string type = 3; } message RuleChainConnectionInfoProto { + option deprecated = true; int32 fromIndex = 1; int64 targetRuleChainIdMSB = 2; int64 targetRuleChainIdLSB = 3; @@ -186,184 +192,196 @@ message DashboardUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional int64 customerIdMSB = 4; - optional int64 customerIdLSB = 5; - string title = 6; - string configuration = 7; - optional string assignedCustomers = 8; - optional string image = 9; - optional int32 mobileOrder = 10; - bool mobileHide = 11; + optional int64 customerIdMSB = 4 [deprecated = true]; + optional int64 customerIdLSB = 5 [deprecated = true]; + string title = 6 [deprecated = true]; + string configuration = 7 [deprecated = true]; + optional string assignedCustomers = 8 [deprecated = true]; + optional string image = 9 [deprecated = true]; + optional int32 mobileOrder = 10 [deprecated = true]; + bool mobileHide = 11 [deprecated = true]; + string entity = 12; } message DeviceUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional int64 customerIdMSB = 4; - optional int64 customerIdLSB = 5; - optional int64 deviceProfileIdMSB = 6; - optional int64 deviceProfileIdLSB = 7; - string name = 8; - string type = 9; - optional string label = 10; - optional string additionalInfo = 11; + optional int64 customerIdMSB = 4 [deprecated = true]; + optional int64 customerIdLSB = 5 [deprecated = true]; + optional int64 deviceProfileIdMSB = 6 [deprecated = true]; + optional int64 deviceProfileIdLSB = 7 [deprecated = true]; + string name = 8 [deprecated = true]; + string type = 9 [deprecated = true]; + optional string label = 10 [deprecated = true]; + optional string additionalInfo = 11 [deprecated = true]; optional string conflictName = 12 [deprecated = true]; - optional int64 firmwareIdMSB = 13; - optional int64 firmwareIdLSB = 14; - optional bytes deviceDataBytes = 15; - optional int64 softwareIdMSB = 16; - optional int64 softwareIdLSB = 17; + optional int64 firmwareIdMSB = 13 [deprecated = true]; + optional int64 firmwareIdLSB = 14 [deprecated = true]; + optional bytes deviceDataBytes = 15 [deprecated = true]; + optional int64 softwareIdMSB = 16 [deprecated = true]; + optional int64 softwareIdLSB = 17 [deprecated = true]; + string entity = 18; } message DeviceProfileUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string name = 4; - optional string description = 5; - bool default = 6; - string type = 7; - optional string transportType = 8; - optional string provisionType = 9; - int64 defaultRuleChainIdMSB = 10; - int64 defaultRuleChainIdLSB = 11; - string defaultQueueName = 12; - bytes profileDataBytes = 13; - optional string provisionDeviceKey = 14; - optional bytes image = 15; - optional int64 firmwareIdMSB = 16; - optional int64 firmwareIdLSB = 17; - optional int64 softwareIdMSB = 18; - optional int64 softwareIdLSB = 19; - optional int64 defaultDashboardIdMSB = 20; - optional int64 defaultDashboardIdLSB = 21; + string name = 4 [deprecated = true]; + optional string description = 5 [deprecated = true]; + bool default = 6 [deprecated = true]; + string type = 7 [deprecated = true]; + optional string transportType = 8 [deprecated = true]; + optional string provisionType = 9 [deprecated = true]; + int64 defaultRuleChainIdMSB = 10 [deprecated = true]; + int64 defaultRuleChainIdLSB = 11 [deprecated = true]; + string defaultQueueName = 12 [deprecated = true]; + bytes profileDataBytes = 13 [deprecated = true]; + optional string provisionDeviceKey = 14 [deprecated = true]; + optional bytes image = 15 [deprecated = true]; + optional int64 firmwareIdMSB = 16 [deprecated = true]; + optional int64 firmwareIdLSB = 17 [deprecated = true]; + optional int64 softwareIdMSB = 18 [deprecated = true]; + optional int64 softwareIdLSB = 19 [deprecated = true]; + optional int64 defaultDashboardIdMSB = 20 [deprecated = true]; + optional int64 defaultDashboardIdLSB = 21 [deprecated = true]; + string entity = 22; } message AssetProfileUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string name = 4; - optional string description = 5; - bool default = 6; - int64 defaultRuleChainIdMSB = 7; - int64 defaultRuleChainIdLSB = 8; - int64 defaultDashboardIdMSB = 9; - int64 defaultDashboardIdLSB = 10; - optional string defaultQueueName = 11; - optional bytes image = 12; + string name = 4 [deprecated = true]; + optional string description = 5 [deprecated = true]; + bool default = 6 [deprecated = true]; + int64 defaultRuleChainIdMSB = 7 [deprecated = true]; + int64 defaultRuleChainIdLSB = 8 [deprecated = true]; + int64 defaultDashboardIdMSB = 9 [deprecated = true]; + int64 defaultDashboardIdLSB = 10 [deprecated = true]; + optional string defaultQueueName = 11 [deprecated = true]; + optional bytes image = 12 [deprecated = true]; + string entity = 13; } message DeviceCredentialsUpdateMsg { - int64 deviceIdMSB = 1; - int64 deviceIdLSB = 2; - string credentialsType = 3; - string credentialsId = 4; - optional string credentialsValue = 5; + int64 deviceIdMSB = 1 [deprecated = true]; + int64 deviceIdLSB = 2 [deprecated = true]; + string credentialsType = 3 [deprecated = true]; + string credentialsId = 4 [deprecated = true]; + optional string credentialsValue = 5 [deprecated = true]; + string entity = 6; } message AssetUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional int64 customerIdMSB = 4; - optional int64 customerIdLSB = 5; - string name = 6; - string type = 7; - optional string label = 8; - optional string additionalInfo = 9; - optional int64 assetProfileIdMSB = 10; - optional int64 assetProfileIdLSB = 11; + optional int64 customerIdMSB = 4 [deprecated = true]; + optional int64 customerIdLSB = 5 [deprecated = true]; + string name = 6 [deprecated = true]; + string type = 7 [deprecated = true]; + optional string label = 8 [deprecated = true]; + optional string additionalInfo = 9 [deprecated = true]; + optional int64 assetProfileIdMSB = 10 [deprecated = true]; + optional int64 assetProfileIdLSB = 11 [deprecated = true]; + string entity = 12; } message EntityViewUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional int64 customerIdMSB = 4; - optional int64 customerIdLSB = 5; - string name = 6; - string type = 7; - int64 entityIdMSB = 8; - int64 entityIdLSB = 9; - EdgeEntityType entityType = 10; - optional string additionalInfo = 11; + optional int64 customerIdMSB = 4 [deprecated = true]; + optional int64 customerIdLSB = 5 [deprecated = true]; + string name = 6 [deprecated = true]; + string type = 7 [deprecated = true]; + int64 entityIdMSB = 8 [deprecated = true]; + int64 entityIdLSB = 9 [deprecated = true]; + EdgeEntityType entityType = 10 [deprecated = true]; + optional string additionalInfo = 11 [deprecated = true]; + string entity = 12; } message AlarmUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string name = 4; - string type = 5; - string originatorType = 6; - string originatorName = 7; - string severity = 8; - string status = 9; - int64 startTs = 10; - int64 endTs = 11; - int64 ackTs = 12; - int64 clearTs = 13; - string details = 14; - bool propagate = 15; - bool propagateToOwner = 16; - bool propagateToTenant = 17; + string name = 4 [deprecated = true]; + string type = 5 [deprecated = true]; + string originatorType = 6 [deprecated = true]; + string originatorName = 7 [deprecated = true]; + string severity = 8 [deprecated = true]; + string status = 9 [deprecated = true]; + int64 startTs = 10 [deprecated = true]; + int64 endTs = 11 [deprecated = true]; + int64 ackTs = 12 [deprecated = true]; + int64 clearTs = 13 [deprecated = true]; + string details = 14 [deprecated = true]; + bool propagate = 15 [deprecated = true]; + bool propagateToOwner = 16 [deprecated = true]; + bool propagateToTenant = 17 [deprecated = true]; + string entity = 18; } message CustomerUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string title = 4; - optional string country = 5; - optional string state = 6; - optional string city = 7; - optional string address = 8; - optional string address2 = 9; - optional string zip = 10; - optional string phone = 11; - optional string email = 12; - optional string additionalInfo = 13; + string title = 4 [deprecated = true]; + optional string country = 5 [deprecated = true]; + optional string state = 6 [deprecated = true]; + optional string city = 7 [deprecated = true]; + optional string address = 8 [deprecated = true]; + optional string address2 = 9 [deprecated = true]; + optional string zip = 10 [deprecated = true]; + optional string phone = 11 [deprecated = true]; + optional string email = 12 [deprecated = true]; + optional string additionalInfo = 13 [deprecated = true]; + string entity = 14; } message RelationUpdateMsg { UpdateMsgType msgType = 1; - int64 fromIdMSB = 2; - int64 fromIdLSB = 3; - string fromEntityType = 4; - int64 toIdMSB = 5; - int64 toIdLSB = 6; - string toEntityType = 7; - string type = 8; - optional string typeGroup = 9; - string additionalInfo = 10; + int64 fromIdMSB = 2 [deprecated = true]; + int64 fromIdLSB = 3 [deprecated = true]; + string fromEntityType = 4 [deprecated = true]; + int64 toIdMSB = 5 [deprecated = true]; + int64 toIdLSB = 6 [deprecated = true]; + string toEntityType = 7 [deprecated = true]; + string type = 8 [deprecated = true]; + optional string typeGroup = 9 [deprecated = true]; + string additionalInfo = 10 [deprecated = true]; + string entity = 11; } message UserUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional int64 customerIdMSB = 4; - optional int64 customerIdLSB = 5; - string email = 6; - string authority = 7; - optional string firstName = 8; - optional string lastName = 9; - optional string additionalInfo = 10; + optional int64 customerIdMSB = 4 [deprecated = true]; + optional int64 customerIdLSB = 5 [deprecated = true]; + string email = 6 [deprecated = true]; + string authority = 7 [deprecated = true]; + optional string firstName = 8 [deprecated = true]; + optional string lastName = 9 [deprecated = true]; + optional string additionalInfo = 10 [deprecated = true]; + string entity = 11; } message WidgetsBundleUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string title = 4; - string alias = 5; - optional bytes image = 6; - bool isSystem = 7; - optional string description = 8; + string title = 4 [deprecated = true]; + string alias = 5 [deprecated = true]; + optional bytes image = 6 [deprecated = true]; + bool isSystem = 7 [deprecated = true]; + optional string description = 8 [deprecated = true]; optional string widgets = 9; - optional int32 order = 10; + optional int32 order = 10 [deprecated = true]; + string entity = 11; } message WidgetTypeUpdateMsg { @@ -372,70 +390,76 @@ message WidgetTypeUpdateMsg { int64 idLSB = 3; optional string bundleAlias = 4 [deprecated = true]; optional string alias = 5 [deprecated = true]; - optional string name = 6; - optional string descriptorJson = 7; - bool isSystem = 8; - optional string image = 9; - optional string description = 10; - optional string fqn = 11; - bool deprecated = 12; - repeated string tags = 13; + optional string name = 6 [deprecated = true]; + optional string descriptorJson = 7 [deprecated = true]; + bool isSystem = 8 [deprecated = true]; + optional string image = 9 [deprecated = true]; + optional string description = 10 [deprecated = true]; + optional string fqn = 11 [deprecated = true]; + bool deprecated = 12 [deprecated = true]; + repeated string tags = 13 [deprecated = true]; + string entity = 14; } message AdminSettingsUpdateMsg { - bool isSystem = 1; - string key = 2; - string jsonValue = 3; + bool isSystem = 1 [deprecated = true]; + string key = 2 [deprecated = true]; + string jsonValue = 3 [deprecated = true]; + string entity = 4; } message UserCredentialsUpdateMsg { - int64 userIdMSB = 1; - int64 userIdLSB = 2; - bool enabled = 3; - string password = 4; + int64 userIdMSB = 1 [deprecated = true]; + int64 userIdLSB = 2 [deprecated = true]; + bool enabled = 3 [deprecated = true]; + string password = 4 [deprecated = true]; + string entity = 5; } message TenantUpdateMsg { UpdateMsgType msgType = 1; - int64 idMSB = 2; - int64 idLSB = 3; - string title = 4; - int64 profileIdMSB = 5; - int64 profileIdLSB = 6; - string region = 7; - optional string country = 8; - optional string state = 9; - optional string city = 10; - optional string address = 11; - optional string address2 = 12; - optional string zip = 13; - optional string phone = 14; - optional string email = 15; - optional string additionalInfo = 16; + int64 idMSB = 2 [deprecated = true]; + int64 idLSB = 3 [deprecated = true]; + string title = 4 [deprecated = true]; + int64 profileIdMSB = 5 [deprecated = true]; + int64 profileIdLSB = 6 [deprecated = true]; + string region = 7 [deprecated = true]; + optional string country = 8 [deprecated = true]; + optional string state = 9 [deprecated = true]; + optional string city = 10 [deprecated = true]; + optional string address = 11 [deprecated = true]; + optional string address2 = 12 [deprecated = true]; + optional string zip = 13 [deprecated = true]; + optional string phone = 14 [deprecated = true]; + optional string email = 15 [deprecated = true]; + optional string additionalInfo = 16 [deprecated = true]; + string entity = 17; } message TenantProfileUpdateMsg { UpdateMsgType msgType = 1; - int64 idMSB = 2; - int64 idLSB = 3; - string name = 4; - optional string description = 5; - bool default = 6; - bool isolatedRuleChain = 7; - bytes profileDataBytes = 8; + int64 idMSB = 2 [deprecated = true]; + int64 idLSB = 3 [deprecated = true]; + string name = 4 [deprecated = true]; + optional string description = 5 [deprecated = true]; + bool default = 6 [deprecated = true]; + bool isolatedRuleChain = 7 [deprecated = true]; + bytes profileDataBytes = 8 [deprecated = true]; + string entity = 9; } message ResourceUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - string title = 4; - string resourceType = 5; - string resourceKey = 6; - string fileName = 7; - optional string data = 8; - optional string etag = 9; - bool isSystem = 10; + string title = 4 [deprecated = true]; + string resourceType = 5 [deprecated = true]; + string resourceKey = 6 [deprecated = true]; + string fileName = 7 [deprecated = true]; + optional string data = 8 [deprecated = true]; + optional string etag = 9 [deprecated = true]; + bool isSystem = 10 [deprecated = true]; + string entity = 11; } message RuleChainMetadataRequestMsg { @@ -519,44 +543,48 @@ message OtaPackageUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - int64 deviceProfileIdMSB = 4; - int64 deviceProfileIdLSB = 5; - string type = 6; - string title = 7; - string version = 8; - string tag = 9; - optional string url = 10; - optional string fileName = 11; - optional string contentType = 12; - optional string checksumAlgorithm = 13; - optional string checksum = 14; - optional int64 dataSize = 15; - optional bytes data = 16; - optional string additionalInfo = 17; + int64 deviceProfileIdMSB = 4 [deprecated = true]; + int64 deviceProfileIdLSB = 5 [deprecated = true]; + string type = 6 [deprecated = true]; + string title = 7 [deprecated = true]; + string version = 8 [deprecated = true]; + string tag = 9 [deprecated = true]; + optional string url = 10 [deprecated = true]; + optional string fileName = 11 [deprecated = true]; + optional string contentType = 12 [deprecated = true]; + optional string checksumAlgorithm = 13 [deprecated = true]; + optional string checksum = 14 [deprecated = true]; + optional int64 dataSize = 15 [deprecated = true]; + optional bytes data = 16 [deprecated = true]; + optional string additionalInfo = 17 [deprecated = true]; + string entity = 18; } message QueueUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - int64 tenantIdMSB = 4; - int64 tenantIdLSB = 5; - string name = 6; - string topic = 7; - int32 pollInterval = 8; - int32 partitions = 9; - bool consumerPerPartition = 10; - int64 packProcessingTimeout = 11; - SubmitStrategyProto submitStrategy = 12; - ProcessingStrategyProto processingStrategy = 13; + int64 tenantIdMSB = 4 [deprecated = true]; + int64 tenantIdLSB = 5 [deprecated = true]; + string name = 6 [deprecated = true]; + string topic = 7 [deprecated = true]; + int32 pollInterval = 8 [deprecated = true]; + int32 partitions = 9 [deprecated = true]; + bool consumerPerPartition = 10 [deprecated = true]; + int64 packProcessingTimeout = 11 [deprecated = true]; + SubmitStrategyProto submitStrategy = 12 [deprecated = true]; + ProcessingStrategyProto processingStrategy = 13 [deprecated = true]; + string entity = 14; } message SubmitStrategyProto { + option deprecated = true; string type = 1; int32 batchSize = 2; } message ProcessingStrategyProto { + option deprecated = true; string type = 1; int32 retries = 2; double failurePercentage = 3; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 0a39d8d51b..c3d3623d8e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -93,6 +93,8 @@ public enum MsgType { DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG, + DEVICE_DELETE_TO_DEVICE_ACTOR_MSG, + DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG, DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG, diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceDeleteMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceDeleteMsg.java new file mode 100644 index 0000000000..1bdab7409b --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceDeleteMsg.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.rule.engine; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; + +@Data +public class DeviceDeleteMsg implements ToDeviceActorNotificationMsg { + + private static final long serialVersionUID = 4679029228395462172L; + + private final TenantId tenantId; + private final DeviceId deviceId; + + @Override + public MsgType getMsgType() { + return MsgType.DEVICE_DELETE_TO_DEVICE_ACTOR_MSG; + } +} diff --git a/common/pom.xml b/common/pom.xml index b00b13db57..76387a1773 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -48,6 +48,7 @@ edge-api version-control script + proto diff --git a/common/proto/pom.xml b/common/proto/pom.xml new file mode 100644 index 0000000000..4ccf154ead --- /dev/null +++ b/common/proto/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.thingsboard + 3.6.2-SNAPSHOT + common + + org.thingsboard.common + proto + jar + + Thingsboard Server Common Protobuf and gRPC structures + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + data + + + org.thingsboard.common + message + + + com.google.protobuf + protobuf-java + + + com.google.protobuf + protobuf-java-util + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.awaitility + awaitility + test + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + + + + thingsboard-repo-deploy + ThingsBoard Repo Deployment + https://repo.thingsboard.io/artifactory/libs-release-public + + + + \ No newline at end of file diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/AdaptorException.java similarity index 94% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java rename to common/proto/src/main/java/org/thingsboard/server/common/adaptor/AdaptorException.java index 8b2908a4b5..1c22163c1f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/AdaptorException.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.transport.adaptor; +package org.thingsboard.server.common.adaptor; public class AdaptorException extends Exception { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java similarity index 99% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java rename to common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 51e74d5097..c51bc784a6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.transport.adaptor; +package org.thingsboard.server.common.adaptor; import com.google.gson.Gson; import com.google.gson.JsonArray; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverterConfig.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverterConfig.java similarity index 96% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverterConfig.java rename to common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverterConfig.java index 0a9df150e5..e160c051f5 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverterConfig.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverterConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.transport.adaptor; +package org.thingsboard.server.common.adaptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java similarity index 99% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java rename to common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java index 54358709f0..098108c864 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.transport.adaptor; +package org.thingsboard.server.common.adaptor; import com.google.gson.Gson; import com.google.gson.JsonElement; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java rename to common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 36038568bc..fd9e35b860 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue; +package org.thingsboard.server.common.util; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; @@ -46,6 +46,7 @@ import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; @@ -384,6 +385,21 @@ public class ProtoUtils { ); } + private static TransportProtos.DeviceDeleteMsgProto toProto(DeviceDeleteMsg msg) { + return TransportProtos.DeviceDeleteMsgProto.newBuilder() + .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits()) + .setDeviceIdMSB(msg.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(msg.getDeviceId().getId().getLeastSignificantBits()) + .build(); + } + + private static DeviceDeleteMsg fromProto(TransportProtos.DeviceDeleteMsgProto proto) { + return new DeviceDeleteMsg( + TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), + new DeviceId(new UUID(proto.getDeviceIdMSB(), proto.getDeviceIdLSB()))); + } + public static TransportProtos.ToDeviceActorNotificationMsgProto toProto(ToDeviceActorNotificationMsg msg) { if (msg instanceof DeviceEdgeUpdateMsg) { DeviceEdgeUpdateMsg updateMsg = (DeviceEdgeUpdateMsg) msg; @@ -413,6 +429,10 @@ public class ProtoUtils { RemoveRpcActorMsg updateMsg = (RemoveRpcActorMsg) msg; TransportProtos.RemoveRpcActorMsgProto proto = toProto(updateMsg); return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setRemoveRpcActorMsg(proto).build(); + } else if (msg instanceof DeviceDeleteMsg) { + DeviceDeleteMsg updateMsg = (DeviceDeleteMsg) msg; + TransportProtos.DeviceDeleteMsgProto proto = toProto(updateMsg); + return TransportProtos.ToDeviceActorNotificationMsgProto.newBuilder().setDeviceDeleteMsg(proto).build(); } return null; } @@ -432,6 +452,8 @@ public class ProtoUtils { return fromProto(proto.getFromDeviceRpcResponseMsg()); } else if (proto.hasRemoveRpcActorMsg()) { return fromProto(proto.getRemoveRpcActorMsg()); + } else if (proto.hasDeviceDeleteMsg()) { + return fromProto(proto.getDeviceDeleteMsg()); } return null; } diff --git a/common/cluster-api/src/main/proto/jsinvoke.proto b/common/proto/src/main/proto/jsinvoke.proto similarity index 100% rename from common/cluster-api/src/main/proto/jsinvoke.proto rename to common/proto/src/main/proto/jsinvoke.proto diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto similarity index 98% rename from common/cluster-api/src/main/proto/queue.proto rename to common/proto/src/main/proto/queue.proto index 016a40f5dc..15e08ee905 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -255,6 +255,12 @@ message GetOrCreateDeviceFromGatewayRequestMsg { message GetOrCreateDeviceFromGatewayResponseMsg { DeviceInfoProto deviceInfo = 1; bytes profileBody = 2; + TransportApiRequestErrorCode error = 3; +} + +enum TransportApiRequestErrorCode { + UNKNOWN_TRANSPORT_API_ERROR = 0; + ENTITY_LIMIT = 1; } message GetEntityProfileRequestMsg { @@ -300,6 +306,12 @@ message CoreStartupMsg { int64 ts = 3; } +message ResourceCacheInvalidateMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string resourceKey = 3; +} + message LwM2MRegistrationRequestMsg { string tenantId = 1; string endpoint = 2; @@ -1004,6 +1016,13 @@ message RemoveRpcActorMsgProto { int64 deviceIdLSB = 6; } +message DeviceDeleteMsgProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; +} + message ToDeviceActorNotificationMsgProto { DeviceEdgeUpdateMsgProto deviceEdgeUpdateMsg = 1; DeviceNameOrTypeUpdateMsgProto deviceNameOrTypeMsg = 2; @@ -1012,6 +1031,7 @@ message ToDeviceActorNotificationMsgProto { ToDeviceRpcRequestActorMsgProto toDeviceRpcRequestMsg = 5; FromDeviceRpcResponseActorMsgProto fromDeviceRpcResponseMsg = 6; RemoveRpcActorMsgProto removeRpcActorMsg = 7; + DeviceDeleteMsgProto deviceDeleteMsg = 8; } /** @@ -1267,6 +1287,7 @@ message ToCoreNotificationMsg { EdgeEventUpdateMsgProto edgeEventUpdate = 14; ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 15; FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 16; + ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 17; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/common/transport/transport-api/src/main/proto/transport.proto b/common/proto/src/main/proto/transport.proto similarity index 100% rename from common/transport/transport-api/src/main/proto/transport.proto rename to common/proto/src/main/proto/transport.proto diff --git a/common/transport/transport-api/src/test/java/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java similarity index 98% rename from common/transport/transport-api/src/test/java/JsonConverterTest.java rename to common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 39ed04a29a..8c32677afb 100644 --- a/common/transport/transport-api/src/test/java/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.thingsboard.server.common.adaptor; + import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java similarity index 99% rename from application/src/test/java/org/thingsboard/server/service/queue/ProtoUtilsTest.java rename to common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index 1a696b730d..a41b2be208 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue; +package org.thingsboard.server.common.util; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java index 97b7b47843..1a84b80cd2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.pubsub; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.gax.core.FixedExecutorProvider; import com.google.cloud.pubsub.v1.stub.GrpcSubscriberStub; import com.google.cloud.pubsub.v1.stub.SubscriberStub; import com.google.cloud.pubsub.v1.stub.SubscriberStubSettings; @@ -76,6 +77,7 @@ public class TbPubSubConsumerTemplate extends AbstractPara SubscriberStubSettings.defaultGrpcTransportProviderBuilder() .setMaxInboundMessageSize(pubSubSettings.getMaxMsgSize()) .build()) + .setExecutorProvider(pubSubSettings.getExecutorProvider()) .build(); this.subscriber = GrpcSubscriberStub.create(subscriberStubSettings); } catch (IOException e) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java index 0d84a8f839..3107855ab1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java @@ -18,6 +18,7 @@ package org.thingsboard.server.queue.pubsub; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; +import com.google.api.gax.core.FixedExecutorProvider; import com.google.cloud.pubsub.v1.Publisher; import com.google.gson.Gson; import com.google.protobuf.ByteString; @@ -120,7 +121,10 @@ public class TbPubSubProducerTemplate implements TbQueuePr try { admin.createTopicIfNotExists(topic); ProjectTopicName topicName = ProjectTopicName.of(pubSubSettings.getProjectId(), topic); - Publisher publisher = Publisher.newBuilder(topicName).setCredentialsProvider(pubSubSettings.getCredentialsProvider()).build(); + Publisher publisher = Publisher.newBuilder(topicName) + .setCredentialsProvider(pubSubSettings.getCredentialsProvider()) + .setExecutorProvider(pubSubSettings.getExecutorProvider()) + .build(); publisherMap.put(topic, publisher); return publisher; } catch (IOException e) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSettings.java index fe9d85c306..145661fed1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSettings.java @@ -17,16 +17,20 @@ package org.thingsboard.server.queue.pubsub; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.FixedExecutorProvider; import com.google.auth.oauth2.ServiceAccountCredentials; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.concurrent.Executors; @Slf4j @ConditionalOnExpression("'${queue.type:null}'=='pubsub'") @@ -46,6 +50,16 @@ public class TbPubSubSettings { @Value("${queue.pubsub.max_messages}") private int maxMessages; + @Value("${queue.pubsub.executor_thread_pool_size:0}") + private int threadPoolSize; + + /** + * Refers to com.google.cloud.pubsub.v1.Publisher default executor configuration + */ + private static final int THREADS_PER_CPU = 5; + + private FixedExecutorProvider executorProvider; + private CredentialsProvider credentialsProvider; @PostConstruct @@ -53,6 +67,17 @@ public class TbPubSubSettings { ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( new ByteArrayInputStream(serviceAccount.getBytes())); credentialsProvider = FixedCredentialsProvider.create(credentials); + if (threadPoolSize == 0) { + threadPoolSize = THREADS_PER_CPU * Runtime.getRuntime().availableProcessors(); + } + executorProvider = FixedExecutorProvider + .create(Executors.newScheduledThreadPool(threadPoolSize, ThingsBoardThreadFactory.forName("pubsub-queue-executor"))); } + @PreDestroy + private void destroy() { + if (executorProvider != null) { + executorProvider.getExecutor().shutdownNow(); + } + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/ProtoWithFSTService.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/ProtoWithFSTService.java index b0d0b182e9..204502068c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/ProtoWithFSTService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/ProtoWithFSTService.java @@ -17,8 +17,10 @@ package org.thingsboard.server.queue.util; import lombok.extern.slf4j.Slf4j; import org.nustaq.serialization.FSTConfiguration; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.FSTUtils; +import org.thingsboard.server.common.data.FstStatsService; import java.util.Optional; @@ -26,12 +28,21 @@ import java.util.Optional; @Service public class ProtoWithFSTService implements DataDecodingEncodingService { + @Autowired + private FstStatsService fstStatsService; + public static final FSTConfiguration CONFIG = FSTConfiguration.createDefaultConfiguration(); @Override public Optional decode(byte[] byteArray) { try { - return Optional.ofNullable(FSTUtils.decode(byteArray)); + long startTime = System.nanoTime(); + Optional optional = Optional.ofNullable(FSTUtils.decode(byteArray)); + optional.ifPresent(obj -> { + fstStatsService.recordDecodeTime(obj.getClass(), startTime); + fstStatsService.incrementDecode(obj.getClass()); + }); + return optional; } catch (IllegalArgumentException e) { log.error("Error during deserialization message, [{}]", e.getMessage()); return Optional.empty(); @@ -41,7 +52,11 @@ public class ProtoWithFSTService implements DataDecodingEncodingService { @Override public byte[] encode(T msq) { - return FSTUtils.encode(msq); + long startTime = System.nanoTime(); + var bytes = FSTUtils.encode(msq); + fstStatsService.recordEncodeTime(msq.getClass(), startTime); + fstStatsService.incrementEncode(msq.getClass()); + return bytes; } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 31bd634221..4a3f2d306b 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -182,12 +182,12 @@ public class TbUtils { return TbJson.parse(ctx, jsonStr); } - public static String bytesToString(List bytesList) { + public static String bytesToString(List bytesList) { byte[] bytes = bytesFromList(bytesList); return new String(bytes); } - public static String bytesToString(List bytesList, String charsetName) throws UnsupportedEncodingException { + public static String bytesToString(List bytesList, String charsetName) throws UnsupportedEncodingException { byte[] bytes = bytesFromList(bytesList); return new String(bytes, charsetName); } @@ -210,10 +210,20 @@ public class TbUtils { } } - private static byte[] bytesFromList(List bytesList) { + private static byte[] bytesFromList(List bytesList) { byte[] bytes = new byte[bytesList.size()]; for (int i = 0; i < bytesList.size(); i++) { - bytes[i] = bytesList.get(i); + Object objectVal = bytesList.get(i); + if (objectVal instanceof Integer) { + bytes[i] = isValidIntegerToByte((Integer) objectVal); + } else if (objectVal instanceof String) { + bytes[i] = isValidIntegerToByte(parseInt((String) objectVal)); + } else if (objectVal instanceof Byte) { + bytes[i] = (byte) objectVal; + } else { + throw new NumberFormatException("The value '" + objectVal + "' could not be correctly converted to a byte. " + + "Must be a HexDecimal/String/Integer/Byte format !"); + } } return bytes; } @@ -643,7 +653,7 @@ public class TbUtils { } } - public static boolean isValidRadix(String value, int radix) { + private static boolean isValidRadix(String value, int radix) { for (int i = 0; i < value.length(); i++) { if (i == 0 && value.charAt(i) == '-') { if (value.length() == 1) @@ -657,4 +667,12 @@ public class TbUtils { return true; } + private static byte isValidIntegerToByte (Integer val) { + if (val > 255 || val.intValue() < -128) { + throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!"); + } else { + return val.byteValue(); + } + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index d2900d6ec4..7d5ae00dcf 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.Random; @@ -385,6 +386,95 @@ public class TbUtilsTest { Assert.assertThrows(IllegalAccessException.class, () -> TbUtils.stringToBytes(ctx, ((ExecutionHashMap) finalInputJson).get("hello"), "UTF-8")); } + @Test + public void bytesFromList() { + byte[] arrayBytes = {(byte)0x00, (byte)0x08, (byte)0x10, (byte)0x1C, (byte)0xFF, (byte)0xFC, (byte)0xAD, (byte)0x88, (byte)0x75, (byte)0x74, (byte)0x8A, (byte)0x82}; + Object[] arrayMix = { "0x00", 8, "16", "0x1C", 255, (byte)0xFC, 173, 136, 117, 116, -118, "-126"}; + + String expected = new String(arrayBytes); + ArrayList listBytes = new ArrayList<>(arrayBytes.length); + for (Byte element : arrayBytes) { + listBytes.add(element); + } + Assert.assertEquals(expected, TbUtils.bytesToString(listBytes)); + + ArrayList listMix = new ArrayList<>(arrayMix.length); + for (Object element : arrayMix) { + listMix.add(element); + } + Assert.assertEquals(expected, TbUtils.bytesToString(listMix)); + } + + @Test + public void bytesFromList_Error() { + List listHex = new ArrayList<>(); + listHex.add("0xFG"); + try { + TbUtils.bytesToString(listHex); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("Failed radix: [16] for value: \"FG\"!")); + } + listHex.add(0, "1F"); + try { + TbUtils.bytesToString(listHex); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("Failed radix: [10] for value: \"1F\"!")); + } + + List listIntString = new ArrayList<>(); + listIntString.add("-129"); + try { + TbUtils.bytesToString(listIntString); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); + } + + listIntString.add(0, "256"); + try { + TbUtils.bytesToString(listIntString); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); + } + + ArrayList listIntBytes = new ArrayList<>(); + listIntBytes.add(-129); + try { + TbUtils.bytesToString(listIntBytes); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); + } + + listIntBytes.add(0, 256); + try { + TbUtils.bytesToString(listIntBytes); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); + } + + ArrayList listObjects = new ArrayList<>(); + ArrayList listStringObjects = new ArrayList<>(); + listStringObjects.add("0xFD"); + listObjects.add(listStringObjects); + try { + TbUtils.bytesToString(listObjects); + Assert.fail("Should throw NumberFormatException"); + } catch (NumberFormatException e) { + Assert.assertTrue(e.getMessage().contains("The value '[0xFD]' could not be correctly converted to a byte. " + + "Must be a HexDecimal/String/Integer/Byte format !")); + } + } + + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/FstStatsServiceImpl.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/FstStatsServiceImpl.java new file mode 100644 index 0000000000..b5cb25f2dc --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/FstStatsServiceImpl.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.stats; + +import io.micrometer.core.instrument.Timer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.FstStatsService; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Service +public class FstStatsServiceImpl implements FstStatsService { + private final ConcurrentHashMap encodeCounters = new ConcurrentHashMap<>(); + private final ConcurrentHashMap decodeCounters = new ConcurrentHashMap<>(); + private final ConcurrentHashMap encodeTimers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap decodeTimer = new ConcurrentHashMap<>(); + + @Autowired + private StatsFactory statsFactory; + + @Override + public void incrementEncode(Class clazz) { + encodeCounters.computeIfAbsent(clazz.getSimpleName(), key -> statsFactory.createStatsCounter("fst_encode", key)).increment(); + } + + @Override + public void incrementDecode(Class clazz) { + decodeCounters.computeIfAbsent(clazz.getSimpleName(), key -> statsFactory.createStatsCounter("fst_decode", key)).increment(); + } + + @Override + public void recordEncodeTime(Class clazz, long startTime) { + encodeTimers.computeIfAbsent(clazz.getSimpleName(), + key -> statsFactory.createTimer("fst_encode_time", "statsName", key)).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + } + + @Override + public void recordDecodeTime(Class clazz, long startTime) { + decodeTimer.computeIfAbsent(clazz.getSimpleName(), + key -> statsFactory.createTimer("fst_decode_time", "statsName", key)).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + } + +} 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 bd02d9fcc4..63979ce500 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 @@ -35,8 +35,8 @@ import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java index 710f18a8b7..629e0380e0 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java @@ -17,7 +17,7 @@ package org.thingsboard.server.transport.coap.adaptors; import org.eclipse.californium.core.coap.Request; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Arrays; 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 05a38e1ad1..e88251c93c 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 @@ -19,7 +19,7 @@ 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; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; 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 d81ff32edd..fffe1a7e1a 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 @@ -29,8 +29,8 @@ import org.eclipse.californium.core.coap.Response; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.CoapTransportResource; 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 4d3b0015ef..5287645591 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 @@ -28,9 +28,9 @@ import org.eclipse.californium.core.coap.Response; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.CoapTransportResource; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java index 6dc5cccf38..ca46d989e4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java @@ -81,6 +81,7 @@ public abstract class AbstractSyncSessionCallback implements SessionMsgListener protected void respond(Response response) { response.getOptions().setContentFormat(TbCoapContentFormatUtil.getContentFormat(exchange.getRequestOptions().getContentFormat(), state.getContentFormat())); + response.setConfirmable(exchange.advanced().getRequest().isConfirmable()); exchange.respond(response); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java index db843a0ea3..04ce043331 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java @@ -34,7 +34,9 @@ public class CoapOkCallback implements TransportServiceCallback { @Override public void onSuccess(Void msg) { - exchange.respond(new Response(onSuccessResponse)); + Response response = new Response(onSuccessResponse); + response.setConfirmable(isConRequest()); + exchange.respond(response); } @Override diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java index 907e49f600..caacd31012 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java @@ -20,7 +20,7 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.server.resources.CoapExchange; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.client.TbCoapClientState; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java index 2209ea6613..fc35da0bd6 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java @@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.server.resources.CoapExchange; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.client.TbCoapClientState; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java index a8531a7fba..a98a04ce28 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java @@ -19,7 +19,7 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.transport.coap.CoapSessionMsgType; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 26effe7058..f4346946a7 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -53,7 +53,7 @@ import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; 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.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 1c0a599082..b3304c6a97 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -33,8 +33,8 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.coap.ConfigProtos; @@ -56,7 +56,6 @@ import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import static com.google.gson.JsonParser.parseString; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java index ce75608a7d..fa6394d41b 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java @@ -19,8 +19,8 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 041ae0510d..589e93d803 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 3dd13cd171..7ba78193b7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -152,8 +152,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { private ObjectModel getObjectModel(String key) { Optional tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key); - return tbResource.map(resource -> helper.parseFromXmlToObjectModel( - Base64.getDecoder().decode(resource.getData()), + return tbResource.map(resource -> helper.parseFromXmlToObjectModel(resource.getData(), key + ".xml")).orElse(null); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java index feb74b47f9..afe1d160ce 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java @@ -19,8 +19,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java index 96e6008c63..fe24b21053 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.adaptors; import com.google.gson.JsonElement; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Collection; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 3bafbb518e..87f6a47150 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -64,7 +64,7 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; 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.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java index 40be642a1a..73bba60cff 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java @@ -21,7 +21,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 513fd97cf6..a30749b17d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -30,8 +30,8 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index 3764c2a9a9..10599d0be7 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -24,7 +24,7 @@ import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; 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 39176fa212..3ff4faf8e8 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 @@ -25,12 +25,12 @@ import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index fd0297203d..64aa448931 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -35,13 +35,14 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportApiProtos; @@ -215,8 +216,7 @@ public abstract class AbstractGatewaySessionHandler callback); - void process(GetOrCreateDeviceFromGatewayRequestMsg msg, + void process(TenantId tenantId, GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback); void process(ProvisionDeviceRequestMsg msg, diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java new file mode 100644 index 0000000000..3bfab1e3f0 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +@Service +@TbTransportComponent +@Slf4j +public class DefaultEntityLimitsCache implements EntityLimitsCache { + + private static final int DEVIATION = 10; + private final Cache cache; + + public DefaultEntityLimitsCache(@Value("${cache.entityLimits.timeToLiveInMinutes:5}") int ttl, + @Value("${cache.entityLimits.maxSize:100000}") int maxSize) { + // We use the 'random' expiration time to avoid peak loads. + long mainPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * (100 - DEVIATION); + long randomPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * DEVIATION; + cache = Caffeine.newBuilder() + .expireAfter(new Expiry() { + @Override + public long expireAfterCreate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime) { + return mainPart + (long) (randomPart * ThreadLocalRandom.current().nextDouble()); + } + + @Override + public long expireAfterUpdate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { + return currentDuration; + } + + @Override + public long expireAfterRead(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { + return currentDuration; + } + }) + .maximumSize(maxSize) + .build(); + } + + @Override + public boolean get(EntityLimitKey key) { + var result = cache.getIfPresent(key); + return result != null ? result : false; + } + + @Override + public void put(EntityLimitKey key, boolean value) { + cache.put(key, value); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java new file mode 100644 index 0000000000..f8957f780d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class EntityLimitKey { + + private final TenantId tenantId; + private final String deviceName; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java new file mode 100644 index 0000000000..b350a2eb27 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public interface EntityLimitsCache { + + boolean get(EntityLimitKey key); + + void put(EntityLimitKey key, boolean value); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index cc4a1bc3df..c4840b2402 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -76,6 +76,8 @@ import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.limits.EntityLimitKey; +import org.thingsboard.server.common.transport.limits.EntityLimitsCache; import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; @@ -161,6 +163,7 @@ public class DefaultTransportService implements TransportService { @Value("${transport.stats.enabled:false}") private boolean statsEnabled; + @Autowired @Lazy private TbApiUsageReportClient apiUsageClient; @@ -184,6 +187,8 @@ public class DefaultTransportService implements TransportService { private final TransportResourceCache transportResourceCache; private final NotificationRuleProcessor notificationRuleProcessor; + private final EntityLimitsCache entityLimitsCache; + protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; protected TbQueueProducer> tbCoreMsgProducer; @@ -212,7 +217,8 @@ public class DefaultTransportService implements TransportService { TransportTenantProfileCache tenantProfileCache, TransportRateLimitService rateLimitService, DataDecodingEncodingService dataDecodingEncodingService, SchedulerComponent scheduler, TransportResourceCache transportResourceCache, - ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor) { + ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor, + EntityLimitsCache entityLimitsCache) { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.queueProvider = queueProvider; @@ -227,6 +233,7 @@ public class DefaultTransportService implements TransportService { this.transportResourceCache = transportResourceCache; this.eventPublisher = eventPublisher; this.notificationRuleProcessor = notificationRuleProcessor; + this.entityLimitsCache = entityLimitsCache; } @PostConstruct @@ -249,7 +256,7 @@ public class DefaultTransportService implements TransportService { } @AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE) - private void start() { + public void start() { mainConsumerExecutor.execute(() -> { while (!stopped) { try { @@ -473,24 +480,33 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } + @Override - public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { + public void process(TenantId tenantId, TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); log.trace("Processing msg: {}", requestMsg); - ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { - TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); - GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); - if (msg.hasDeviceInfo()) { - TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); - result.deviceInfo(tdi); - ByteString profileBody = msg.getProfileBody(); - if (profileBody != null && !profileBody.isEmpty()) { - result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + var key = new EntityLimitKey(tenantId, StringUtils.truncate(requestMsg.getDeviceName(), 256)); + if (entityLimitsCache.get(key)) { + transportCallbackExecutor.submit(() -> callback.onError(new RuntimeException(DataConstants.MAXIMUM_NUMBER_OF_DEVICES_REACHED))); + } else { + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); + GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); + if (msg.hasDeviceInfo()) { + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (!profileBody.isEmpty()) { + result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + } + } else if (TransportProtos.TransportApiRequestErrorCode.ENTITY_LIMIT.equals(msg.getError())) { + entityLimitsCache.put(key, true); + throw new RuntimeException(DataConstants.MAXIMUM_NUMBER_OF_DEVICES_REACHED); } - } - return result.build(); - }, MoreExecutors.directExecutor()); - AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } } @Override diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExecutorProvider.java b/common/util/src/main/java/org/thingsboard/common/util/ExecutorProvider.java new file mode 100644 index 0000000000..61379c8c89 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ExecutorProvider.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.common.util; + +import java.util.concurrent.ScheduledExecutorService; + +public interface ExecutorProvider { + + ScheduledExecutorService getExecutor(); +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index ee2f91ec4d..499c0b55a5 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; @@ -61,6 +62,9 @@ public class JacksonUtil { .configure(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature(), false) .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) .build(); + public static final ObjectMapper IGNORE_UNKNOWN_PROPERTIES_JSON_MAPPER = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); public static ObjectMapper getObjectMapperWithJavaTimeModule() { return new ObjectMapper().registerModule(new JavaTimeModule()); @@ -106,6 +110,14 @@ public class JacksonUtil { } } + public static T fromStringIgnoreUnknownProperties(String string, Class clazz) { + try { + return string != null ? IGNORE_UNKNOWN_PROPERTIES_JSON_MAPPER.readValue(string, clazz) : null; + } catch (IOException e) { + throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e); + } + } + public static T fromBytes(byte[] bytes, Class clazz) { try { return bytes != null ? OBJECT_MAPPER.readValue(bytes, clazz) : null; diff --git a/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java b/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java index 87d1e11bab..bbeb86efca 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java +++ b/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java @@ -19,6 +19,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.util.function.UnaryOperator; +import java.util.regex.Matcher; import java.util.regex.Pattern; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -37,4 +38,14 @@ public class RegexUtils { return pattern.matcher(input).matches(); } + public static String getMatch(String input, Pattern pattern, int group) { + Matcher matcher = pattern.matcher(input); + if (matcher.find()) { + try { + return matcher.group(group); + } catch (Exception ignored) {} + } + return null; + } + } diff --git a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java index 2ba2384acd..57fd457f14 100644 --- a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java +++ b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java @@ -19,11 +19,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; + +import java.util.UUID; public class JacksonUtilTest { @Test - public void allow_unquoted_field_mapper_test() { + public void allowUnquotedFieldMapperTest() { String data = "{data: 123}"; JsonNode actualResult = JacksonUtil.toJsonNode(data, JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); // should be: {"data": 123} ObjectNode expectedResult = JacksonUtil.newObjectNode(); @@ -32,4 +36,23 @@ public class JacksonUtilTest { Assert.assertThrows(IllegalArgumentException.class, () -> JacksonUtil.toJsonNode(data)); // syntax exception due to missing quotes in the field name! } -} \ No newline at end of file + @Test + public void failOnUnknownPropertiesMapperTest() { + Asset asset = new Asset(); + asset.setId(new AssetId(UUID.randomUUID())); + asset.setName("Test"); + asset.setType("type"); + String serializedAsset = JacksonUtil.toString(asset); + JsonNode jsonNode = JacksonUtil.toJsonNode(serializedAsset); + // case: add new field to serialized Asset string and check for backward compatibility with original Asset object + Assert.assertNotNull(jsonNode); + ((ObjectNode) jsonNode).put("test", (String) null); + serializedAsset = JacksonUtil.toString(jsonNode); + // deserialize with FAIL_ON_UNKNOWN_PROPERTIES = false + Asset result = JacksonUtil.fromStringIgnoreUnknownProperties(serializedAsset, Asset.class); + Assert.assertNotNull(result); + Assert.assertEquals(asset.getId(), result.getId()); + Assert.assertEquals(asset.getName(), result.getName()); + Assert.assertEquals(asset.getType(), result.getType()); + } +} diff --git a/dao/pom.xml b/dao/pom.xml index 3c0a036212..d3145fe892 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -234,6 +234,18 @@ io.hypersistence hypersistence-utils-hibernate-55 + + org.apache.xmlgraphics + batik-transcoder + + + org.apache.xmlgraphics + batik-codec + + + com.drewnoakes + metadata-extractor + diff --git a/dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java b/dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java index 169d7c3b88..5ac75f94f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.page.PageLink; import java.util.UUID; -public interface ExportableEntityDao> extends Dao { +public interface ExportableEntityDao> extends Dao { T findByTenantIdAndExternalId(UUID tenantId, UUID externalId); @@ -30,6 +30,10 @@ public interface ExportableEntityDao findByTenantId(UUID tenantId, PageLink pageLink); + default PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return findByTenantId(tenantId, pageLink).mapData(ExportableEntity::getId); + } + I getExternalIdByInternal(I internalId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ImageContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ImageContainerDao.java new file mode 100644 index 0000000000..162ce75dc0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ImageContainerDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface ImageContainerDao> { + + List findByTenantAndImageLink(TenantId tenantId, String imageUrl, int limit); + + List findByImageLink(String imageUrl, int limit); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 5e7c3ff455..37d5dcea7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -24,8 +24,10 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java index b7fc5fedc6..87e9177d66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java @@ -23,10 +23,11 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ImageContainerDao; import java.util.UUID; -public interface AssetProfileDao extends Dao, ExportableEntityDao { +public interface AssetProfileDao extends Dao, ExportableEntityDao, ImageContainerDao { AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, UUID assetProfileId); @@ -43,4 +44,7 @@ public interface AssetProfileDao extends Dao, ExportableEntityDao< AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId); AssetProfile findByName(TenantId tenantId, String profileName); + + PageData findAllWithImages(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 9240bbc223..86cafcd10b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -71,6 +72,9 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService assetProfileValidator; + @Autowired + private ImageService imageService; + @TransactionalEventListener(classes = AssetProfileEvictEvent.class) @Override public void handleEvictEvent(AssetProfileEvictEvent event) { @@ -90,18 +94,28 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService assetProfileDao.findById(tenantId, assetProfileId.getId()), true); + return cache.getOrFetchFromDB(AssetProfileCacheKey.fromId(assetProfileId), + () -> assetProfileDao.findById(tenantId, assetProfileId.getId()), true, putInCache); } @Override public AssetProfile findAssetProfileByName(TenantId tenantId, String profileName) { + return findAssetProfileByName(tenantId, profileName, true); + } + + @Override + public AssetProfile findAssetProfileByName(TenantId tenantId, String profileName, boolean putInCache) { log.trace("Executing findAssetProfileByName [{}][{}]", tenantId, profileName); Validator.validateString(profileName, INCORRECT_ASSET_PROFILE_NAME + profileName); - return cache.getAndPutInTransaction(AssetProfileCacheKey.fromName(tenantId, profileName), - () -> assetProfileDao.findByName(tenantId, profileName), false); + return cache.getOrFetchFromDB(AssetProfileCacheKey.fromName(tenantId, profileName), + () -> assetProfileDao.findByName(tenantId, profileName), false, putInCache); } @Override @@ -127,10 +141,11 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService, TenantEntityDao, Exportabl PageData findIdsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAllIds(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 887c39109e..1d1cf36fff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -16,16 +16,20 @@ package org.thingsboard.server.dao.dashboard; import org.thingsboard.server.common.data.DashboardInfo; +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.widget.WidgetTypeInfo; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ImageContainerDao; +import java.util.List; import java.util.UUID; /** * The Interface DashboardInfoDao. */ -public interface DashboardInfoDao extends Dao { +public interface DashboardInfoDao extends Dao, ImageContainerDao { /** * Find dashboards by tenantId and page link. @@ -78,4 +82,5 @@ public interface DashboardInfoDao extends Dao { DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name); String findTitleById(UUID tenantId, UUID dashboardId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index be1739bc7a..f050e924c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -50,6 +50,7 @@ import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -78,6 +79,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private EdgeDao edgeDao; + @Autowired + private ImageService imageService; + @Autowired private DataValidator dashboardValidator; @@ -153,6 +157,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); } try { + imageService.replaceBase64WithImageUrl(dashboard); var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) @@ -170,6 +175,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } + + @Override public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId) { Dashboard dashboard = findDashboardById(tenantId, dashboardId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 9f67093529..50396c6ebc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.util.DeviceConnectivityUtil; import java.io.InputStream; import java.io.InputStreamReader; -import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -64,6 +63,7 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.LINUX; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getHost; @Service("DeviceConnectivityDaoService") @Slf4j @@ -137,14 +137,15 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId); + String deviceName = device.getName(); ObjectNode commands = JacksonUtil.newObjectNode(); if (isEnabled(MQTT)) { - Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTT)) + Optional.ofNullable(getGatewayDockerCommands(baseUrl, deviceName, creds, MQTT)) .ifPresent(v -> commands.set(MQTT, v)); } if (isEnabled(MQTTS)) { - Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTTS)) + Optional.ofNullable(getGatewayDockerCommands(baseUrl, deviceName, creds, MQTTS)) .ifPresent(v -> commands.set(MQTTS, v)); } return commands; @@ -230,7 +231,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService deviceCredentials.getCredentialsType() != DeviceCredentialsType.ACCESS_TOKEN) { return null; } - String hostName = getHost(baseUrl, properties); + String hostName = getHost(baseUrl, properties, protocol); String propertiesPort = properties.getPort(); String port = (propertiesPort.isEmpty() || HTTP_DEFAULT_PORT.equals(propertiesPort) || HTTPS_DEFAULT_PORT.equals(propertiesPort)) ? "" : ":" + propertiesPort; @@ -278,14 +279,14 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { DeviceConnectivityInfo properties = getConnectivity(MQTT); - String mqttHost = getHost(baseUrl, properties); + String mqttHost = getHost(baseUrl, properties, MQTT); String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); } private List getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { DeviceConnectivityInfo properties = getConnectivity(MQTTS); - String mqttHost = getHost(baseUrl, properties); + String mqttHost = getHost(baseUrl, properties, MQTTS); String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); @@ -298,21 +299,21 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService return null; } - private JsonNode getGatewayDockerCommands(String baseUrl, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException { + private JsonNode getGatewayDockerCommands(String baseUrl, String deviceName, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException { ObjectNode dockerLaunchCommands = JacksonUtil.newObjectNode(); DeviceConnectivityInfo properties = getConnectivity(mqttType); - String mqttHost = getHost(baseUrl, properties); + String mqttHost = getHost(baseUrl, properties, mqttType); String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); - Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(LINUX, mqttHost, mqttPort, deviceCredentials)) + Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(LINUX, deviceName, mqttHost, mqttPort, deviceCredentials)) .ifPresent(v -> dockerLaunchCommands.put(LINUX, v)); - Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(WINDOWS, mqttHost, mqttPort, deviceCredentials)) + Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(WINDOWS, deviceName, mqttHost, mqttPort, deviceCredentials)) .ifPresent(v -> dockerLaunchCommands.put(WINDOWS, v)); return dockerLaunchCommands.isEmpty() ? null : dockerLaunchCommands; } private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { DeviceConnectivityInfo properties = getConnectivity(protocol); - String mqttHost = getHost(baseUrl, properties); + String mqttHost = getHost(baseUrl, properties, protocol); String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); } @@ -352,20 +353,16 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { DeviceConnectivityInfo properties = getConnectivity(protocol); - String hostName = getHost(baseUrl, properties); + String hostName = getHost(baseUrl, properties, protocol); String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials); } private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { DeviceConnectivityInfo properties = getConnectivity(protocol); - String host = getHost(baseUrl, properties); + String host = getHost(baseUrl, properties, protocol); String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials); } - private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException { - return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost(); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index c221002413..7bca5ebf8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -23,10 +23,11 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ImageContainerDao; import java.util.UUID; -public interface DeviceProfileDao extends Dao, ExportableEntityDao { +public interface DeviceProfileDao extends Dao, ExportableEntityDao, ImageContainerDao { DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId); @@ -45,4 +46,7 @@ public interface DeviceProfileDao extends Dao, ExportableEntityDa DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey); DeviceProfile findByName(TenantId tenantId, String profileName); + + PageData findAllWithImages(PageLink pageLink); + } 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 31aaf54206..847811cd4f 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 @@ -47,6 +47,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -91,6 +92,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true); + return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromId(deviceProfileId), + () -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true, putInCache); } @Override public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { + return findDeviceProfileByName(tenantId, profileName, true); + } + + @Override + public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName, boolean putInCache) { log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName); validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName); - return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName), - () -> deviceProfileDao.findByName(tenantId, profileName), true); + return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromName(tenantId, profileName), + () -> deviceProfileDao.findByName(tenantId, profileName), true, putInCache); } @Override @@ -164,10 +178,11 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService entityViewDao.findById(tenantId, entityViewId.getId()) - , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); + , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true, putInCache); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java new file mode 100644 index 0000000000..2f618e94d5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.exception; + +import lombok.Getter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; + +public class EntitiesLimitException extends DataValidationException { + private static final long serialVersionUID = -9211462514373279196L; + + @Getter + private final TenantId tenantId; + @Getter + private final EntityType entityType; + + public EntitiesLimitException(TenantId tenantId, EntityType entityType) { + super(entityType.getNormalName() + "s limit reached"); + this.tenantId = tenantId; + this.entityType = entityType; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 10dc226e7a..21cea59ee3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -499,6 +499,8 @@ public class ModelConstants { public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; public static final String RESOURCE_DATA_COLUMN = "data"; public static final String RESOURCE_ETAG_COLUMN = "etag"; + public static final String RESOURCE_DESCRIPTOR_COLUMN = "descriptor"; + public static final String RESOURCE_PREVIEW_COLUMN = "preview"; /** * Ota Package constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index 4fbc50fd69..ff63994259 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -15,24 +15,30 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; @@ -42,8 +48,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @Data @EqualsAndHashCode(callSuper = true) @Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = RESOURCE_TABLE_NAME) -public class TbResourceEntity extends BaseSqlEntity implements BaseEntity { +public class TbResourceEntity extends BaseSqlEntity { @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") private UUID tenantId; @@ -64,11 +71,21 @@ public class TbResourceEntity extends BaseSqlEntity implements BaseE private String fileName; @Column(name = RESOURCE_DATA_COLUMN) - private String data; + private byte[] data; @Column(name = RESOURCE_ETAG_COLUMN) private String etag; + @Type(type = "json") + @Column(name = RESOURCE_DESCRIPTOR_COLUMN) + private JsonNode descriptor; + + @Column(name = RESOURCE_PREVIEW_COLUMN) + private byte[] preview; + + @Column(name = EXTERNAL_ID_PROPERTY) + private UUID externalId; + public TbResourceEntity() { } @@ -87,6 +104,9 @@ public class TbResourceEntity extends BaseSqlEntity implements BaseE this.fileName = resource.getFileName(); this.data = resource.getData(); this.etag = resource.getEtag(); + this.descriptor = resource.getDescriptor(); + this.preview = resource.getPreview(); + this.externalId = getUuid(resource.getExternalId()); } @Override @@ -101,6 +121,9 @@ public class TbResourceEntity extends BaseSqlEntity implements BaseE resource.setFileName(fileName); resource.setData(data); resource.setEtag(etag); + resource.setDescriptor(descriptor); + resource.setPreview(preview); + resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index e8504bc388..3a693a6301 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -15,21 +15,28 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.ResourceType; 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.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; @@ -40,6 +47,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER @Data @EqualsAndHashCode(callSuper = true) @Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = RESOURCE_TABLE_NAME) public class TbResourceInfoEntity extends BaseSqlEntity implements BaseEntity { @@ -59,7 +67,17 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen private String searchText; @Column(name = RESOURCE_ETAG_COLUMN) - private String hashCode; + private String etag; + + @Column(name = RESOURCE_FILE_NAME_COLUMN) + private String fileName; + + @Type(type = "json") + @Column(name = RESOURCE_DESCRIPTOR_COLUMN) + private JsonNode descriptor; + + @Column(name = EXTERNAL_ID_PROPERTY) + private UUID externalId; public TbResourceInfoEntity() { } @@ -74,7 +92,10 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.resourceType = resource.getResourceType().name(); this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); - this.hashCode = resource.getEtag(); + this.etag = resource.getEtag(); + this.fileName = resource.getFileName(); + this.descriptor = resource.getDescriptor(); + this.externalId = getUuid(resource.getExternalId()); } @Override @@ -86,7 +107,10 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); - resource.setEtag(hashCode); + resource.setEtag(etag); + resource.setFileName(fileName); + resource.setDescriptor(descriptor); + resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java new file mode 100644 index 0000000000..ad3934f76e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -0,0 +1,615 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.resource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Base64Utils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.ImageDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbImageDeleteResult; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceInfoFilter; +import org.thingsboard.server.common.data.id.HasId; +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 org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.ImageContainerDao; +import org.thingsboard.server.dao.asset.AssetProfileDao; +import org.thingsboard.server.dao.dashboard.DashboardInfoDao; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.service.validator.ResourceDataValidator; +import org.thingsboard.server.dao.util.ImageUtils; +import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage; +import org.thingsboard.server.dao.util.JsonNodeProcessingTask; +import org.thingsboard.server.dao.util.JsonPathProcessingTask; +import org.thingsboard.server.dao.widget.WidgetTypeDao; +import org.thingsboard.server.dao.widget.WidgetsBundleDao; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.regex.Pattern; + +@Service +@Slf4j +public class BaseImageService extends BaseResourceService implements ImageService { + + private static final int MAX_ENTITIES_TO_FIND = 10; + + public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); + public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); + + static { + DASHBOARD_BASE64_MAPPING.put("settings.dashboardLogoUrl", "$prefix logo"); + DASHBOARD_BASE64_MAPPING.put("states.default.layouts.main.gridSettings.backgroundImageUrl", "$prefix background"); + DASHBOARD_BASE64_MAPPING.put("states.default.layouts.right.gridSettings.backgroundImageUrl", "$prefix right background"); + DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.main.gridSettings.backgroundImageUrl", "$prefix $stateId background"); + DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.right.gridSettings.backgroundImageUrl", "$prefix $stateId right background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.backgroundImageUrl", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.mapImageUrl", "$prefix widget \"$title\" map image"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImage", "$prefix widget \"$title\" marker image"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImages", "$prefix widget \"$title\" marker image $index"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageUrl", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageBase64", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].datasources.*.dataKeys.*.settings.customIcon", "$prefix widget \"$title\" custom icon"); + + WIDGET_TYPE_BASE64_MAPPING.put("settings.backgroundImageUrl", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.mapImageUrl", "$prefix map image"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImage", "Map marker image"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImages", "Map marker image $index"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageUrl", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageBase64", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("datasources.*.dataKeys.*.settings.customIcon", "$prefix custom icon"); + } + + private final AssetProfileDao assetProfileDao; + private final DeviceProfileDao deviceProfileDao; + private final WidgetsBundleDao widgetsBundleDao; + private final WidgetTypeDao widgetTypeDao; + private final DashboardInfoDao dashboardInfoDao; + private final Map> imageContainerDaoMap = new HashMap<>(); + + public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, + AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, + WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { + super(resourceDao, resourceInfoDao, resourceValidator); + this.assetProfileDao = assetProfileDao; + this.deviceProfileDao = deviceProfileDao; + this.widgetsBundleDao = widgetsBundleDao; + this.widgetTypeDao = widgetTypeDao; + this.dashboardInfoDao = dashboardInfoDao; + } + + @PostConstruct + public void init() { + imageContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + imageContainerDaoMap.put(EntityType.WIDGETS_BUNDLE, widgetsBundleDao); + imageContainerDaoMap.put(EntityType.DEVICE_PROFILE, deviceProfileDao); + imageContainerDaoMap.put(EntityType.ASSET_PROFILE, assetProfileDao); + imageContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + } + + + @Override + @SneakyThrows + public TbResourceInfo saveImage(TbResource image) { + if (image.getId() == null && StringUtils.isEmpty(image.getResourceKey())) { + image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName())); + } + resourceValidator.validate(image, TbResourceInfo::getTenantId); + + ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class); + Pair result = processImage(image.getData(), descriptor); + descriptor = result.getLeft(); + image.setEtag(descriptor.getEtag()); + image.setDescriptorValue(descriptor); + image.setPreview(result.getRight()); + + log.debug("[{}] Creating image {} ('{}')", image.getTenantId(), image.getResourceKey(), image.getName()); + return new TbResourceInfo(doSaveResource(image)); + } + + private Pair processImage(byte[] data, ImageDescriptor descriptor) throws Exception { + ProcessedImage image = ImageUtils.processImage(data, descriptor.getMediaType(), 250); + ProcessedImage preview = image.getPreview(); + + descriptor.setWidth(image.getWidth()); + descriptor.setHeight(image.getHeight()); + descriptor.setSize(image.getSize()); + descriptor.setEtag(calculateEtag(data)); + + ImageDescriptor previewDescriptor = new ImageDescriptor(); + previewDescriptor.setWidth(preview.getWidth()); + previewDescriptor.setHeight(preview.getHeight()); + previewDescriptor.setMediaType(preview.getMediaType()); + previewDescriptor.setSize(preview.getSize()); + previewDescriptor.setEtag(preview.getData() != null ? calculateEtag(preview.getData()) : descriptor.getEtag()); + descriptor.setPreviewDescriptor(previewDescriptor); + + return Pair.of(descriptor, preview.getData()); + } + + private String getUniqueKey(TenantId tenantId, String filename) { + if (!resourceInfoDao.existsByTenantIdAndResourceTypeAndResourceKey(tenantId, ResourceType.IMAGE, filename)) { + return filename; + } + + String basename = StringUtils.substringBeforeLast(filename, "."); + String extension = StringUtils.substringAfterLast(filename, "."); + + Set existing = resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix( + tenantId, ResourceType.IMAGE, basename + ); + String resourceKey = filename; + int idx = 1; + while (existing.contains(resourceKey)) { + resourceKey = basename + "_(" + idx + ")." + extension; + idx++; + } + log.debug("[{}] Generated unique key {} for image {}", tenantId, resourceKey, filename); + return resourceKey; + } + + @Override + public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) { + log.trace("Executing saveImageInfo [{}] [{}]", imageInfo.getTenantId(), imageInfo.getId()); + return saveResource(new TbResource(imageInfo)); + } + + @Override + public TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key) { + log.trace("Executing getImageInfoByTenantIdAndKey [{}] [{}]", tenantId, key); + return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key); + } + + @Override + public PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing getImagesByTenantId [{}]", tenantId); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .resourceTypes(Set.of(ResourceType.IMAGE)) + .build(); + return findTenantResourcesByTenantId(filter, pageLink); + } + + @Override + public PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing getAllImagesByTenantId [{}]", tenantId); + TbResourceInfoFilter filter = TbResourceInfoFilter.builder() + .tenantId(tenantId) + .resourceTypes(Set.of(ResourceType.IMAGE)) + .build(); + return findAllTenantResourcesByTenantId(filter, pageLink); + } + + @Override + public byte[] getImageData(TenantId tenantId, TbResourceId imageId) { + log.trace("Executing getImageData [{}] [{}]", tenantId, imageId); + return resourceDao.getResourceData(tenantId, imageId); + } + + @Override + public byte[] getImagePreview(TenantId tenantId, TbResourceId imageId) { + log.trace("Executing getImagePreview [{}] [{}]", tenantId, imageId); + return resourceDao.getResourcePreview(tenantId, imageId); + } + + @Override + public TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force) { + var tenantId = imageInfo.getTenantId(); + var imageId = imageInfo.getId(); + log.trace("Executing deleteImage [{}] [{}]", tenantId, imageId); + Validator.validateId(imageId, INCORRECT_RESOURCE_ID + imageId); + TbImageDeleteResult.TbImageDeleteResultBuilder result = TbImageDeleteResult.builder(); + boolean success = true; + if (!force) { + var link = DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink(); + Map>> affectedEntities = new HashMap<>(); + imageContainerDaoMap.forEach((entityType, imageContainerDao) -> { + var entities = tenantId.isSysTenantId() ? imageContainerDao.findByImageLink(link, MAX_ENTITIES_TO_FIND) : + imageContainerDao.findByTenantAndImageLink(tenantId, link, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + affectedEntities.put(entityType.name(), entities); + } + }); + if (!affectedEntities.isEmpty()) { + success = false; + result.references(affectedEntities); + } + } + if (success) { + deleteResource(tenantId, imageId, force); + } + return result.success(success).build(); + } + + @Override + public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) { + log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag); + return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag); + } + + @Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure + @Override + public boolean replaceBase64WithImageUrl(HasImage entity, String type) { + log.trace("Executing replaceBase64WithImageUrl [{}] [{}] [{}]", entity.getTenantId(), type, entity.getName()); + String imageName = "\"" + entity.getName() + "\" "; + if (entity.getTenantId() == null || entity.getTenantId().isSysTenantId()) { + imageName += "system "; + } + imageName = imageName + type + " image"; + + UpdateResult result = base64ToImageUrl(entity.getTenantId(), imageName, entity.getImage()); + entity.setImage(result.getValue()); + return result.isUpdated(); + } + + @Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure + @Override + public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) { + log.trace("Executing replaceBase64WithImageUrl [{}] [WidgetTypeDetails] [{}]", entity.getTenantId(), entity.getId()); + String prefix = "\"" + entity.getName() + "\" "; + if (entity.getTenantId() == null || entity.getTenantId().isSysTenantId()) { + prefix += "system "; + } + prefix += "widget"; + UpdateResult result = base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage()); + entity.setImage(result.getValue()); + boolean updated = result.isUpdated(); + if (entity.getDescriptor().isObject()) { + ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); + JsonNode defaultConfig = Optional.ofNullable(descriptor.get("defaultConfig")) + .filter(JsonNode::isTextual).map(JsonNode::asText) + .map(JacksonUtil::toJsonNode).orElse(null); + if (defaultConfig != null) { + updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); + descriptor.put("defaultConfig", defaultConfig.toString()); + } + } + updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); + return updated; + } + + @Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure + @Override + public boolean replaceBase64WithImageUrl(Dashboard entity) { + log.trace("Executing replaceBase64WithImageUrl [{}] [Dashboard] [{}]", entity.getTenantId(), entity.getId()); + String prefix = "\"" + entity.getTitle() + "\" dashboard"; + var result = base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage()); + boolean updated = result.isUpdated(); + entity.setImage(result.getValue()); + updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), entity.getConfiguration()); + updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getConfiguration()); + return updated; + } + + private boolean base64ToImageUrlUsingMapping(TenantId tenantId, Map mapping, Map templateParams, JsonNode configuration) { + boolean updated = false; + for (var entry : mapping.entrySet()) { + String expression = entry.getValue(); + Queue tasks = new LinkedList<>(); + tasks.add(new JsonPathProcessingTask(entry.getKey().split("\\."), templateParams, configuration)); + while (!tasks.isEmpty()) { + JsonPathProcessingTask task = tasks.poll(); + String token = task.currentToken(); + JsonNode node = task.getNode(); + if (node == null) { + continue; + } + if (token.equals("*") || token.startsWith("$")) { + String variableName = token.startsWith("$") ? token.substring(1) : null; + if (node.isArray()) { + ArrayNode childArray = (ArrayNode) node; + for (JsonNode element : childArray) { + tasks.add(task.next(element)); + } + } else if (node.isObject()) { + ObjectNode on = (ObjectNode) node; + for (Iterator> it = on.fields(); it.hasNext(); ) { + var kv = it.next(); + if (variableName != null) { + tasks.add(task.next(kv.getValue(), variableName, kv.getKey())); + } else { + tasks.add(task.next(kv.getValue())); + } + } + } + } else { + String variableName = null; + String variableValue = null; + if (token.contains("[$")) { + variableName = StringUtils.substringBetween(token, "[$", "]"); + token = StringUtils.substringBefore(token, "[$"); + } + if (node.has(token)) { + JsonNode value = node.get(token); + if (variableName != null && value.has(variableName) && value.get(variableName).isTextual()) { + variableValue = value.get(variableName).asText(); + } + if (task.isLast()) { + String name = expression; + for (var replacement : task.getVariables().entrySet()) { + name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); + } + if (node.isObject() && value.isTextual()) { + var result = base64ToImageUrl(tenantId, name, value.asText()); + ((ObjectNode) node).put(token, result.getValue()); + updated |= result.isUpdated(); + } else if (value.isArray()) { + ArrayNode array = (ArrayNode) value; + for (int i = 0; i < array.size(); i++) { + String arrayElementName = name.replace("$index", Integer.toString(i)); + UpdateResult result = base64ToImageUrl(tenantId, arrayElementName, array.get(i).asText()); + array.set(i, result.getValue()); + updated |= result.isUpdated(); + } + } + } else { + if (StringUtils.isNotEmpty(variableName)) { + tasks.add(task.next(value, variableName, variableValue)); + } else { + tasks.add(task.next(value)); + } + } + } + } + } + } + return updated; + } + + private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data) { + return base64ToImageUrl(tenantId, name, data, false); + } + + private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:(.*):(.*);data:(.*);.*"); + + private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) { + if (StringUtils.isBlank(data)) { + return UpdateResult.of(false, data); + } + var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data); + boolean matches = matcher.matches(); + String mdResourceKey = null; + String mdResourceName = null; + String mdMediaType; + if (matches) { + mdResourceKey = new String(Base64Utils.decodeFromString(matcher.group(1)), StandardCharsets.UTF_8); + mdResourceName = new String(Base64Utils.decodeFromString(matcher.group(2)), StandardCharsets.UTF_8); + mdMediaType = matcher.group(3); + } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) { + mdMediaType = StringUtils.substringBetween(data, "data:", ";base64"); + } else { + return UpdateResult.of(false, data); + } + String base64Data = StringUtils.substringAfter(data, "base64,"); + String extension = ImageUtils.mediaTypeToFileExtension(mdMediaType); + byte[] imageData = Base64.getDecoder().decode(base64Data); + String etag = calculateEtag(imageData); + var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag); + if (imageInfo == null) { + TbResource image = new TbResource(); + image.setTenantId(tenantId); + image.setResourceType(ResourceType.IMAGE); + if (StringUtils.isBlank(mdResourceName)) { + mdResourceName = name; + } + image.setTitle(mdResourceName); + + String fileName; + if (StringUtils.isBlank(mdResourceKey)) { + fileName = StringUtils.strip(mdResourceName.toLowerCase() + .replaceAll("['\"]", "") + .replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers + + "." + extension; + } else { + fileName = mdResourceKey; + } + image.setFileName(fileName); + image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType)); + image.setData(imageData); + try { + imageInfo = saveImage(image); + } catch (Exception e) { + if (log.isDebugEnabled()) { // printing stacktrace + log.warn("[{}][{}] Failed to replace Base64 with image url for {}", tenantId, name, StringUtils.abbreviate(data, 50), e); + } else { + log.warn("[{}][{}] Failed to replace Base64 with image url for {}: {}", tenantId, name, StringUtils.abbreviate(data, 50), ExceptionUtils.getMessage(e)); + } + return UpdateResult.of(false, data); + } + } + return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink()); + } + + private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) { + boolean updated = false; + Queue tasks = new LinkedList<>(); + tasks.add(new JsonNodeProcessingTask(title, root)); + while (!tasks.isEmpty()) { + JsonNodeProcessingTask task = tasks.poll(); + JsonNode node = task.getNode(); + if (node == null) { + continue; + } + String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + " "); + if (node.isObject()) { + ObjectNode on = (ObjectNode) node; + for (Iterator it = on.fieldNames(); it.hasNext(); ) { + String childName = it.next(); + JsonNode childValue = on.get(childName); + if (childValue.isTextual()) { + UpdateResult result = base64ToImageUrl(tenantId, currentPath + childName, childValue.asText(), true); + on.put(childName, result.getValue()); + updated |= result.isUpdated(); + } else if (childValue.isObject() || childValue.isArray()) { + tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue)); + } + } + } else if (node.isArray()) { + ArrayNode childArray = (ArrayNode) node; + for (int i = 0; i < childArray.size(); i++) { + JsonNode element = childArray.get(i); + if (element.isObject()) { + tasks.add(new JsonNodeProcessingTask(currentPath + " " + i, element)); + } else if (element.isTextual()) { + UpdateResult result = base64ToImageUrl(tenantId, currentPath + "." + i, element.asText(), true); + childArray.set(i, result.getValue()); + updated |= result.isUpdated(); + } + } + } + } + return updated; + } + + @Override + public void inlineImage(HasImage entity) { + log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName()); + entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); + } + + @Override + public void inlineImages(Dashboard dashboard) { + log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId()); + inlineImage(dashboard); + inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration()); + } + + @Override + public void inlineImages(WidgetTypeDetails widgetTypeDetails) { + log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId()); + inlineImage(widgetTypeDetails); + inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor()); + } + + private void inlineIntoJson(TenantId tenantId, JsonNode root) { + Queue tasks = new LinkedList<>(); + tasks.add(new JsonNodeProcessingTask("", root)); + while (!tasks.isEmpty()) { + JsonNodeProcessingTask task = tasks.poll(); + JsonNode node = task.getNode(); + if (node == null) { + continue; + } + String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + "."); + if (node.isObject()) { + ObjectNode on = (ObjectNode) node; + for (Iterator it = on.fieldNames(); it.hasNext(); ) { + String childName = it.next(); + JsonNode childValue = on.get(childName); + if (childValue.isTextual()) { + on.put(childName, inlineImage(tenantId, currentPath + childName, childValue.asText())); + } else if (childValue.isObject() || childValue.isArray()) { + tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue)); + } + } + } else if (node.isArray()) { + ArrayNode childArray = (ArrayNode) node; + for (int i = 0; i < childArray.size(); i++) { + JsonNode element = childArray.get(i); + if (element.isObject()) { + tasks.add(new JsonNodeProcessingTask(currentPath + "." + i, element)); + } else if (element.isTextual()) { + childArray.set(i, inlineImage(tenantId, currentPath + "." + i, element.asText())); + } + } + } + } + } + + private String inlineImage(TenantId tenantId, String path, String url) { + try { + ImageCacheKey key = getKeyFromUrl(tenantId, url); + if (key != null) { + var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getKey()); + if (imageInfo != null) { + byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); + ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); + String tbImagePrefix = "tb-image:" + Base64Utils.encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":" + + Base64Utils.encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ";"; + return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data); + } + } + } catch (Exception e) { + log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, url, e); + } + return url; + } + + private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) throws JsonProcessingException { + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + return preview ? descriptor.getPreviewDescriptor() : descriptor; + } + + private ImageCacheKey getKeyFromUrl(TenantId tenantId, String url) { + if (StringUtils.isBlank(url)) { + return null; + } + TenantId imageTenantId = null; + if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) { + imageTenantId = tenantId; + } else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) { + imageTenantId = TenantId.SYS_TENANT_ID; + } + if (imageTenantId != null) { + var parts = url.split("/"); + if (parts.length == 5) { + return new ImageCacheKey(imageTenantId, parts[4], false); + } else if (parts.length == 6 && "preview".equals(parts[5])) { + return new ImageCacheKey(imageTenantId, parts[4], true); + } + } + return null; + } + + @Data(staticConstructor = "of") + private static class UpdateResult { + private final boolean updated; + private final String value; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 2181318eed..eb34f434b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.dao.resource; +import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; @@ -38,9 +40,9 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import java.util.List; import java.util.Optional; @@ -51,49 +53,64 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TbResourceDaoService") @Slf4j @AllArgsConstructor +@Primary public class BaseResourceService extends AbstractCachedEntityService implements ResourceService { public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; - private final TbResourceDao resourceDao; - private final TbResourceInfoDao resourceInfoDao; - private final DataValidator resourceValidator; + protected final TbResourceDao resourceDao; + protected final TbResourceInfoDao resourceInfoDao; + protected final ResourceDataValidator resourceValidator; @Override - public TbResource saveResource(TbResource resource) { - return doSaveResource(resource, true); + public TbResource saveResource(TbResource resource, boolean doValidate) { + log.trace("Executing saveResource [{}]", resource); + if (doValidate) { + resourceValidator.validate(resource, TbResourceInfo::getTenantId); + } + if (resource.getData() != null) { + resource.setEtag(calculateEtag(resource.getData())); + } + return doSaveResource(resource); } @Override - public TbResource saveResource(TbResource resource, boolean doValidate) { - return doSaveResource(resource, doValidate); + public TbResource saveResource(TbResource resource) { + return saveResource(resource, true); } - private TbResource doSaveResource(TbResource resource, boolean doValidate) { - if (doValidate) { - resourceValidator.validate(resource, TbResourceInfo::getTenantId); - } + protected TbResource doSaveResource(TbResource resource) { + TenantId tenantId = resource.getTenantId(); try { - TbResource saved = resourceDao.save(resource.getTenantId(), resource); - publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId())); + TbResource saved; + if (resource.getData() != null) { + saved = resourceDao.save(tenantId, resource); + } else { + TbResourceInfo resourceInfo = saveResourceInfo(resource); + saved = new TbResource(resourceInfo); + } + publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId())); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) .entityId(saved.getId()).added(resource.getId() == null).build()); return saved; } catch (Exception t) { - publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId())); + publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId())); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) { - String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName"; - throw new DataValidationException("Resource with such " + field + " already exists!"); + throw new DataValidationException("Resource with such key already exists!"); } else { throw t; } } } + private TbResourceInfo saveResourceInfo(TbResource resource) { + return resourceInfoDao.save(resource.getTenantId(), new TbResourceInfo(resource)); + } + @Override - public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey) { - log.trace("Executing getResource [{}] [{}] [{}]", tenantId, resourceType, resourceKey); - return resourceDao.getResource(tenantId, resourceType, resourceKey); + public TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) { + log.trace("Executing findResourceByTenantIdAndKey [{}] [{}] [{}]", tenantId, resourceType, resourceKey); + return resourceDao.findResourceByTenantIdAndKey(tenantId, resourceType, resourceKey); } @Override @@ -112,6 +129,12 @@ public class BaseResourceService extends AbstractCachedEntityService resourceInfoDao.findById(tenantId, resourceId.getId()), true); } + @Override + public TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) { + log.trace("Executing findResourceInfoByTenantIdAndKey [{}] [{}] [{}]", tenantId, resourceType, resourceKey); + return resourceInfoDao.findByTenantIdAndKey(tenantId, resourceType, resourceKey); + } + @Override public ListenableFuture findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId) { log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); @@ -121,13 +144,21 @@ public class BaseResourceService extends AbstractCachedEntityService findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { TenantId tenantId = filter.getTenantId(); @@ -177,6 +208,11 @@ public class BaseResourceService extends AbstractCachedEntityService tenantResourcesRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java new file mode 100644 index 0000000000..7ee2f4a6b4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.resource; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class ImageCacheKey { + + private final TenantId tenantId; + private final String key; + private final boolean preview; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 4117563805..0a8e215618 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -17,17 +17,19 @@ 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.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 org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; import java.util.List; -public interface TbResourceDao extends Dao, TenantEntityWithDataDao { +public interface TbResourceDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { - TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceId); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); @@ -39,4 +41,11 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao ResourceType resourceType, String[] objectIds, String searchText); + + byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); + + byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId); + + long getResourceSize(TenantId tenantId, TbResourceId resourceId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index f0f0b6fdad..d18ced319e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.dao.resource; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; -import java.util.UUID; +import java.util.List; +import java.util.Set; public interface TbResourceInfoDao extends Dao { @@ -29,4 +32,13 @@ public interface TbResourceInfoDao extends Dao { PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); + TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + + boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + + Set findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(TenantId tenantId, ResourceType resourceType, String prefix); + + List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); + + TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag); } 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 956e4b83f6..6257bb0420 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 @@ -155,7 +155,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (ruleChain == null) { return RuleChainUpdateResult.failed(); } - RuleChainDataValidator.validateMetaData(ruleChainMetaData); + RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); List nodes = ruleChainMetaData.getNodes(); List toAddOrUpdate = new ArrayList<>(); @@ -194,6 +194,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChainId); node = ruleNodeUpdater.apply(node); + RuleChainDataValidator.validateRuleNode(node); RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index d81c05044f..961fffcdc0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -117,7 +118,7 @@ public abstract class DataValidator> { protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { - throw new DataValidationException(entityType.getNormalName() + "s limit reached"); + throw new EntitiesLimitException(tenantId, entityType); } } @@ -128,8 +129,7 @@ public abstract class DataValidator> { EntityType entityType) { if (maxSumDataSize > 0) { if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) { - throw new DataValidationException(String.format("Failed to create the %s, files size limit is exhausted %d bytes!", - entityType.name().toLowerCase().replaceAll("_", " "), maxSumDataSize)); + throw new DataValidationException(String.format("%ss total size exceeds the maximum of " + maxSumDataSize + " bytes", entityType.getNormalName())); } } } @@ -161,7 +161,7 @@ public abstract class DataValidator> { } static void validateQueueNameOrTopic(String value, String fieldName) { - if (StringUtils.isEmpty(value) || value.trim().length() == 0 ) { + if (StringUtils.isEmpty(value) || value.trim().length() == 0) { throw new DataValidationException(String.format("Queue %s should be specified!", fieldName)); } if (!QUEUE_PATTERN.matcher(value).matches()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index d620950628..26d8ab75fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.service.validator; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.widget.BaseWidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -56,34 +55,57 @@ public class ResourceDataValidator extends DataValidator { @Override protected void validateCreate(TenantId tenantId, TbResource resource) { - if (tenantId != null && !TenantId.SYS_TENANT_ID.equals(tenantId)) { - DefaultTenantProfileConfiguration profileConfiguration = - (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); - long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes(); - validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, resource.getData().length(), TB_RESOURCE); + if (resource.getData() == null || resource.getData().length == 0) { + throw new DataValidationException("Resource data should be specified"); + } + } + + @Override + protected TbResource validateUpdate(TenantId tenantId, TbResource resource) { + if (resource.getData() != null && !resource.getResourceType().isUpdatable() && + tenantId != null && !tenantId.isSysTenantId()) { + throw new DataValidationException("This type of resource can't be updated"); } + return resource; } @Override protected void validateDataImpl(TenantId tenantId, TbResource resource) { validateString("Resource title", resource.getTitle()); + if (resource.getTenantId() == null) { + resource.setTenantId(TenantId.SYS_TENANT_ID); + } + if (!resource.getTenantId().isSysTenantId()) { + if (!tenantService.tenantExists(resource.getTenantId())) { + throw new DataValidationException("Resource is referencing to non-existent tenant!"); + } + } if (resource.getResourceType() == null) { throw new DataValidationException("Resource type should be specified!"); } + if (!resource.getTenantId().isSysTenantId() && resource.getData() != null) { + DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); + long maxResourceSize = profileConfiguration.getMaxResourceSize(); + if (maxResourceSize > 0 && resource.getData().length > maxResourceSize) { + throw new IllegalArgumentException("Resource exceeds the maximum size of " + maxResourceSize + " bytes"); + } + long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes(); + int dataSize = resource.getData().length; + if (resource.getId() != null) { + long prevSize = resourceDao.getResourceSize(tenantId, resource.getId()); + dataSize -= prevSize; + } + validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE); + } if (StringUtils.isEmpty(resource.getFileName())) { throw new DataValidationException("Resource file name should be specified!"); } + if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) { + throw new DataValidationException("File name contains forbidden symbols"); + } if (StringUtils.isEmpty(resource.getResourceKey())) { throw new DataValidationException("Resource key should be specified!"); } - if (resource.getTenantId() == null) { - resource.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); - } - if (!resource.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { - if (!tenantService.tenantExists(resource.getTenantId())) { - throw new DataValidationException("Resource is referencing to non-existent tenant!"); - } - } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 43878adc93..76dad19bcd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -87,15 +87,18 @@ public class RuleChainDataValidator extends DataValidator { } public static List validateMetaData(RuleChainMetaData ruleChainMetaData) { - ConstraintValidator.validateFields(ruleChainMetaData); - List throwables = ruleChainMetaData.getNodes().stream() + validateMetaDataFieldsAndConnections(ruleChainMetaData); + return ruleChainMetaData.getNodes().stream() .map(RuleChainDataValidator::validateRuleNode) .filter(Objects::nonNull) .collect(Collectors.toList()); + } + + public static void validateMetaDataFieldsAndConnections(RuleChainMetaData ruleChainMetaData) { + ConstraintValidator.validateFields(ruleChainMetaData); if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { validateCircles(ruleChainMetaData.getConnections()); } - return throwables; } public static Throwable validateRuleNode(RuleNode ruleNode) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index f23478e6d5..a9acef8608 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; +import java.util.List; import java.util.UUID; public interface AssetProfileRepository extends JpaRepository, ExportableEntityRepository { @@ -60,4 +61,14 @@ public interface AssetProfileRepository extends JpaRepository findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, Pageable page); + + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a WHERE a.image = :imageLink") + List findByImageLink(@Param("imageLink") String imageLink, Pageable page); + + Page findAllByImageNotNull(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index e34df2f21c..b57e5c0c40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.asset; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -31,7 +32,7 @@ import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import java.util.Objects; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -97,6 +98,11 @@ public class JpaAssetProfileDao extends JpaAbstractDao findAllWithImages(PageLink pageLink) { + return DaoUtil.toPageData(assetProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink))); + } + @Override public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); @@ -118,6 +124,16 @@ public class JpaAssetProfileDao extends JpaAbstractDao findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) { + return assetProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit)); + } + + @Override + public List findByImageLink(String imageLink, int limit) { + return assetProfileRepository.findByImageLink(imageLink, PageRequest.of(0, limit)); + } + @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index bddda18563..35c765dccf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; +import java.util.List; import java.util.UUID; /** @@ -74,4 +75,16 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + + @Query(nativeQuery = true, + value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt" + ) + List findByImageLink(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java index ee6b59e30b..3adc50d2aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java @@ -43,4 +43,7 @@ public interface DashboardRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT d.id FROM DashboardEntity d") + Page findAllIds(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 9353a4dbde..2a85692459 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -85,6 +85,11 @@ public class JpaDashboardDao extends JpaAbstractDao return DaoUtil.pageToPageData(dashboardRepository.findIdsByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)).map(DashboardId::new)); } + @Override + public PageData findAllIds(PageLink pageLink) { + return DaoUtil.pageToPageData(dashboardRepository.findAllIds(DaoUtil.toPageable(pageLink)).map(DashboardId::new)); + } + @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index b988048db8..c9318db61e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -31,7 +32,6 @@ import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.UUID; /** @@ -123,4 +123,14 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, limit)); + } + + @Override + public List findByImageLink(String imageLink, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findByImageLink(imageLink, limit)); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 295b64cd89..a9baf60e60 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -22,9 +22,11 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; +import java.util.List; import java.util.UUID; public interface DeviceProfileRepository extends JpaRepository, ExportableEntityRepository { @@ -68,7 +70,17 @@ public interface DeviceProfileRepository extends JpaRepository findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, Pageable page); + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + + "FROM DeviceProfileEntity d WHERE d.image = :imageLink") + List findByImageLink(@Param("imageLink") String imageLink, Pageable page); + @Query("SELECT externalId FROM DeviceProfileEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); + Page findAllByImageNotNull(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 7b334330fd..12a91e3cf7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.device; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -34,7 +35,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.Objects; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -115,6 +116,11 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findAllWithImages(PageLink pageLink) { + return DaoUtil.toPageData(deviceProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink))); + } + @Override public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); @@ -136,6 +142,16 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) { + return deviceProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit)); + } + + @Override + public List findByImageLink(String imageLink, int limit) { + return deviceProfileRepository.findByImageLink(imageLink, PageRequest.of(0, limit)); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 641fda927b..2326d2ffe4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +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; @@ -31,7 +32,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; -import java.util.Objects; import java.util.UUID; @Slf4j @@ -56,8 +56,7 @@ public class JpaTbResourceDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { + return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(resourceRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)) + .map(TbResourceId::new)); + } + + @Override + public TbResourceId getExternalIdByInternal(TbResourceId internalId) { + return DaoUtil.toEntityId(resourceRepository.getExternalIdByInternal(internalId.getId()), TbResourceId::new); + } + @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java index 5e69bca62a..d3123fe38c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -25,14 +25,19 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter; 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.util.CollectionsUtil; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; import org.thingsboard.server.dao.resource.TbResourceInfoDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.EnumSet; +import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j @Component @@ -54,25 +59,54 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { - ResourceType resourceType = filter.getResourceType(); + Set resourceTypes = filter.getResourceTypes(); + if (CollectionsUtil.isEmpty(resourceTypes)) { + resourceTypes = EnumSet.allOf(ResourceType.class); + } return DaoUtil.toPageData(resourceInfoRepository .findAllTenantResourcesByTenantId( - filter.getTenantId().getId(), - TenantId.NULL_UUID, - resourceType == null ? null : resourceType.name(), + filter.getTenantId().getId(), TenantId.NULL_UUID, + resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); } @Override public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) { - ResourceType resourceType = filter.getResourceType(); + Set resourceTypes = filter.getResourceTypes(); + if (CollectionsUtil.isEmpty(resourceTypes)) { + resourceTypes = EnumSet.allOf(ResourceType.class); + } return DaoUtil.toPageData(resourceInfoRepository .findTenantResourcesByTenantId( filter.getTenantId().getId(), - resourceType == null ? null : resourceType.name(), + resourceTypes.stream().map(Enum::name).collect(Collectors.toList()), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } + @Override + public TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) { + return DaoUtil.getData(resourceInfoRepository.findByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey)); + } + + @Override + public boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey) { + return resourceInfoRepository.existsByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey); + } + + @Override + public Set findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(TenantId tenantId, ResourceType resourceType, String prefix) { + return resourceInfoRepository.findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(tenantId.getId(), resourceType.name(), prefix); + } + + @Override + public List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) { + return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndEtagAndResourceKeyStartingWith(tenantId.getId(), etag, query)); + } + + @Override + public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag) { + return DaoUtil.getData(resourceInfoRepository.findSystemOrTenantImageByEtag(tenantId.getId(), resourceType.name(), etag)); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index d1540e5611..ced94e1ea8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -22,6 +22,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; +import java.util.List; +import java.util.Set; import java.util.UUID; public interface TbResourceInfoRepository extends JpaRepository { @@ -29,26 +31,43 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, - @Param("systemAdminId") UUID sysadminId, - @Param("resourceType") String resourceType, + @Param("systemTenantId") UUID systemTenantId, + @Param("resourceTypes") List resourceTypes, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " + "ri.tenantId = :tenantId " + - "AND (:resourceType IS NULL OR ri.resourceType = :resourceType)" + + "AND ri.resourceType IN :resourceTypes " + "AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)") Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, - @Param("resourceType") String resourceType, + @Param("resourceTypes") List resourceTypes, @Param("searchText") String searchText, Pageable pageable); + TbResourceInfoEntity findByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey); + + boolean existsByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey); + + @Query(value = "SELECT r.resource_key FROM resource r WHERE r.tenant_id = :tenantId AND r.resource_type = :resourceType " + + "AND starts_with(r.resource_key, :prefix)", nativeQuery = true) + Set findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(@Param("tenantId") UUID tenantId, + @Param("resourceType") String resourceType, + @Param("prefix") String prefix); + + List findByTenantIdAndEtagAndResourceKeyStartingWith(UUID tenantId, String etag, String query); + + @Query(value = "SELECT * FROM resource r WHERE (r.tenant_id = '13814000-1dd2-11b2-8080-808080808080' OR r.tenant_id = :tenantId) " + + "AND r.resource_type = :resourceType AND r.etag = :etag LIMIT 1", nativeQuery = true) + TbResourceInfoEntity findSystemOrTenantImageByEtag(@Param("tenantId") UUID tenantId, + @Param("resourceType") String resourceType, + @Param("etag") String etag); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 5110522b1d..9fd69a37ef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -20,12 +20,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.TbResourceEntity; import java.util.List; import java.util.UUID; -public interface TbResourceRepository extends JpaRepository { +public interface TbResourceRepository extends JpaRepository, ExportableEntityRepository { TbResourceEntity findByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey); @@ -80,4 +81,20 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 7700e71bc4..d105943a80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -121,7 +121,7 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { + public PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) { boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter); boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter); boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty(); @@ -204,6 +204,12 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(widgetTypeRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)) + .map(WidgetTypeId::new)); + } + @Override public WidgetTypeId getExternalIdByInternal(WidgetTypeId internalId) { return Optional.ofNullable(widgetTypeRepository.getExternalIdById(internalId.getId())) @@ -225,6 +231,21 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findAllWidgetTypesIds(PageLink pageLink) { + return DaoUtil.pageToPageData(widgetTypeRepository.findAllIds(DaoUtil.toPageable(pageLink)).map(WidgetTypeId::new)); + } + + @Override + public List findByTenantAndImageLink(TenantId tenantId, String imageUrl, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findByTenantAndImageUrl(tenantId.getId(), imageUrl, limit)); + } + + @Override + public List findByImageLink(String imageUrl, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findByImageUrl(imageUrl, limit)); + } + @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index a5051a2747..27759864d0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -33,7 +33,6 @@ import org.thingsboard.server.dao.widget.WidgetsBundleDao; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -103,6 +102,11 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findAllWidgetsBundles(PageLink pageLink) { + return DaoUtil.toPageData(widgetsBundleRepository.findAll(DaoUtil.toPageable(pageLink))); + } + private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, boolean fullSearch, PageLink pageLink) { if (fullSearch) { return DaoUtil.toPageData( @@ -142,6 +146,16 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findByTenantAndImageLink(TenantId tenantId, String imageUrl, int limit) { + return DaoUtil.convertDataList(widgetsBundleRepository.findByTenantAndImageUrl(tenantId.getId(), imageUrl, limit)); + } + + @Override + public List findByImageLink(String imageUrl, int limit) { + return DaoUtil.convertDataList(widgetsBundleRepository.findByImageUrl(imageUrl, limit)); + } + @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index f9114a9e40..83797d7baa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -194,4 +194,17 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes, Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " + + "(select id from widget_type where tenant_id = :tenantId " + + "and (image = :imageLink or descriptor ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :lmt)" + ) + List findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + + @Query(nativeQuery = true, + value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " + + "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt)" + ) + List findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index f4b3dec5bb..0583d0b47d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -69,11 +69,17 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))", - nativeQuery = true) + nativeQuery = true) List findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId, - @Param("resourceId") UUID resourceId); + @Param("resourceId") UUID resourceId); @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); + @Query("SELECT w.id FROM WidgetTypeDetailsEntity w") + Page findAllIds(Pageable pageable); + + @Query("SELECT w.id FROM WidgetTypeDetailsEntity w WHERE w.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 9db39aab8f..64440fb1f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.ExportableEntityRepository; +import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import java.util.List; @@ -133,4 +134,9 @@ public interface WidgetsBundleRepository extends JpaRepository findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + + @Query(nativeQuery = true, value = "SELECT * FROM widgets_bundle wb WHERE wb.image = :imageLink limit :lmt") + List findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java index c4c3e021c8..43c1a3feea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java @@ -53,11 +53,15 @@ public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeni if (keyId == null) { Optional tsKvDictionaryOptional; tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); - if (!tsKvDictionaryOptional.isPresent()) { + if (tsKvDictionaryOptional.isEmpty()) { tsCreationLock.lock(); try { + keyId = tsKvDictionaryMap.get(strKey); + if (keyId != null) { + return keyId; + } tsKvDictionaryOptional = dictionaryRepository.findById(new TsKvDictionaryCompositeKey(strKey)); - if (!tsKvDictionaryOptional.isPresent()) { + if (tsKvDictionaryOptional.isEmpty()) { TsKvDictionary tsKvDictionary = new TsKvDictionary(); tsKvDictionary.setKey(strKey); try { diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index aa446c12b5..c5708ce34e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -19,9 +19,14 @@ import org.apache.commons.lang3.StringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.device.DeviceConnectivityInfo; -import java.util.Arrays; -import java.util.List; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.regex.Pattern; public class DeviceConnectivityUtil { @@ -39,9 +44,13 @@ public class DeviceConnectivityUtil { public static final String JSON_EXAMPLE_PAYLOAD = "\"{temperature:25}\""; public static final String DOCKER_RUN = "docker run --rm -it "; public static final String GATEWAY_DOCKER_RUN = "docker run -it "; + + public static final String NETWORK_HOST_PARAM = "--network=host "; + public static final String HOST_DOCKER_INTERNAL = "host.docker.internal"; + public static final String ADD_DOCKER_INTERNAL_HOST = "--add-host=" + HOST_DOCKER_INTERNAL + ":host-gateway "; public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients "; public static final String COAP_IMAGE = "thingsboard/coap-clients "; - public static final List LOCAL_HOSTS = Arrays.asList("localhost", "127.0.0.1"); + private final static Pattern VALID_URL_PATTERN = Pattern.compile("^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) { return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD, @@ -71,7 +80,7 @@ public class DeviceConnectivityUtil { command.append(" -u \"").append(credentials.getUserName()).append("\""); } if (credentials.getPassword() != null) { - command.append(" -P \"").append(credentials.getPassword()).append("\"");; + command.append(" -P \"").append(credentials.getPassword()).append("\""); } } else { return null; @@ -84,23 +93,25 @@ public class DeviceConnectivityUtil { return command.toString(); } - public static String getGatewayLaunchCommand(String os, String host, String port, DeviceCredentials deviceCredentials) { - String gatewayVolumePathPrefix = "~/.tb-gateway"; + public static String getGatewayLaunchCommand(String os, String deviceName, String host, String port, DeviceCredentials deviceCredentials) { + String gatewayVolumePathPrefix = "~/.tb-gateway/"; if (WINDOWS.equals(os)) { - gatewayVolumePathPrefix = "%HOMEPATH%/tb-gateway"; + gatewayVolumePathPrefix = "%HOMEDRIVE%%HOMEPATH%\\tb-gateway\\"; } - String gatewayContainerName = "tbGateway" + StringUtils.capitalize(host.replace(".", "")); + String gatewayContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", ""); StringBuilder command = new StringBuilder(GATEWAY_DOCKER_RUN); - command.append("-v {gatewayVolumePathPrefix}/logs:/thingsboard_gateway/logs".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); - command.append(" -v {gatewayVolumePathPrefix}/extensions:/thingsboard_gateway/extensions".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); - command.append(" -v {gatewayVolumePathPrefix}/config:/thingsboard_gateway/config".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); - command.append(" --name ").append(gatewayContainerName); - command.append(" -e host=").append(host); - command.append(" -e port=").append(port); - - switch(deviceCredentials.getCredentialsType()) { + command.append("-v {gatewayVolumePathPrefix}logs:/thingsboard_gateway/logs ".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); + command.append("-v {gatewayVolumePathPrefix}extensions:/thingsboard_gateway/extensions ".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); + command.append("-v {gatewayVolumePathPrefix}config:/thingsboard_gateway/config ".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix)); + command.append("--name ").append(gatewayContainerName).append(" "); + command.append(isLocalhost(host) ? ADD_DOCKER_INTERNAL_HOST : ""); + command.append("-p 60000-61000:60000-61000 "); + command.append("-e host=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append(" "); + command.append("-e port=").append(port); + + switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: command.append(" -e accessToken=").append(deviceCredentials.getCredentialsId()); break; @@ -139,7 +150,11 @@ public class DeviceConnectivityUtil { } StringBuilder mqttDockerCommand = new StringBuilder(); - mqttDockerCommand.append(DOCKER_RUN).append(LOCAL_HOSTS.contains(host) ? "--network=host ":"").append(MQTT_IMAGE); + mqttDockerCommand.append(DOCKER_RUN).append(isLocalhost(host) ? ADD_DOCKER_INTERNAL_HOST : "").append(MQTT_IMAGE); + + if (isLocalhost(host)){ + mqttCommand = mqttCommand.replace(host, HOST_DOCKER_INTERNAL); + } if (MQTTS.equals(protocol)) { mqttDockerCommand.append("/bin/sh -c \"") @@ -171,6 +186,43 @@ public class DeviceConnectivityUtil { public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) { String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials); - return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN + (LOCAL_HOSTS.contains(host) ? "--network=host ":""), COAP_IMAGE, coapCommand) : null; + if (coapCommand != null && isLocalhost(host)) { + coapCommand = coapCommand.replace(host, HOST_DOCKER_INTERNAL); + } + return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN + (isLocalhost(host) ? ADD_DOCKER_INTERNAL_HOST : ""), COAP_IMAGE, coapCommand) : null; + } + + public static String getHost(String baseUrl, DeviceConnectivityInfo properties, String protocol) throws URISyntaxException { + String initialHost = properties.getHost().isEmpty() ? baseUrl : properties.getHost(); + InetAddress inetAddress; + String host = null; + if (VALID_URL_PATTERN.matcher(initialHost).matches()) { + host = new URI(initialHost).getHost(); + } + if (host == null) { + host = initialHost; + } + try { + host = host.replaceAll("^https?://", ""); + inetAddress = InetAddress.getByName(host); + } catch (UnknownHostException e) { + return host; + } + if (inetAddress instanceof Inet6Address) { + host = host.replaceAll("[\\[\\]]", ""); + if (!MQTT.equals(protocol) && !MQTTS.equals(protocol)) { + host = "[" + host + "]"; + } + } + return host; + } + + private static boolean isLocalhost(String host) { + try { + InetAddress inetAddress = InetAddress.getByName(host); + return inetAddress.isLoopbackAddress(); + } catch (UnknownHostException e) { + return false; + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java new file mode 100644 index 0000000000..128f9da304 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -0,0 +1,232 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.bridge.BridgeContext; +import org.apache.batik.bridge.DocumentLoader; +import org.apache.batik.bridge.GVTBuilder; +import org.apache.batik.bridge.UserAgent; +import org.apache.batik.bridge.UserAgentAdapter; +import org.apache.batik.gvt.GraphicsNode; +import org.apache.batik.transcoder.TranscoderInput; +import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.image.PNGTranscoder; +import org.apache.batik.util.XMLResourceDescriptor; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.w3c.dom.Document; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class ImageUtils { + + private static final Map mediaTypeMappings = Map.of( + "jpeg", "jpg", + "svg+xml", "svg", + "x-icon", "ico" + ); + + public static String mediaTypeToFileExtension(String mimeType) { + String subtype = MimeTypeUtils.parseMimeType(mimeType).getSubtype(); + return mediaTypeMappings.getOrDefault(subtype, subtype); + } + + public static String fileExtensionToMediaType(String extension) { + String subtype = mediaTypeMappings.entrySet().stream() + .filter(mapping -> mapping.getValue().equals(extension)) + .map(Map.Entry::getKey).findFirst().orElse(extension); + return new MimeType("image", subtype).toString(); + } + + public static ProcessedImage processImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception { + if (mediaTypeToFileExtension(mediaType).equals("svg")) { + return processSvgImage(data, mediaType, thumbnailMaxDimension); + } + ProcessedImage image = new ProcessedImage(); + image.setMediaType(mediaType); + image.setData(data); + image.setSize(data.length); + + BufferedImage bufferedImage = null; + try { + bufferedImage = ImageIO.read(new ByteArrayInputStream(data)); + } catch (Exception ignored) { + } + if (bufferedImage == null) { // means that media type is not supported by ImageIO; extracting width and height from metadata and leaving preview as original image + Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(data)); + for (Directory dir : metadata.getDirectories()) { + Tag widthTag = dir.getTags().stream() + .filter(tag -> tag.getTagName().toLowerCase().contains("width")) + .findFirst().orElse(null); + Tag heightTag = dir.getTags().stream() + .filter(tag -> tag.getTagName().toLowerCase().contains("height")) + .findFirst().orElse(null); + if (widthTag == null || heightTag == null) { + continue; + } + int width = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); + int height = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); + image.setWidth(width); + image.setHeight(height); + + ProcessedImage preview = new ProcessedImage(); + preview.setWidth(image.getWidth()); + preview.setHeight(image.getHeight()); + preview.setMediaType(mediaType); + preview.setData(null); + preview.setSize(data.length); + image.setPreview(preview); + log.warn("Couldn't process {} ({}) with ImageIO, leaving preview as original image", mediaType, dir.getName()); + return image; + } + log.warn("Image media type {} not supported", mediaType); + throw new IllegalArgumentException("Media type " + mediaType + " not supported"); + } + + image.setWidth(bufferedImage.getWidth()); + image.setHeight(bufferedImage.getHeight()); + + ProcessedImage preview = new ProcessedImage(); + int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension); + preview.setWidth(thumbnailDimensions[0]); + preview.setHeight(thumbnailDimensions[1]); + + if (preview.getWidth() == image.getWidth() && preview.getHeight() == image.getHeight()) { + if (mediaType.equals("image/png")) { + preview.setMediaType(mediaType); + preview.setData(null); + preview.setSize(data.length); + image.setPreview(preview); + return image; + } + } + + BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_RGB); + thumbnail.getGraphics().drawImage(bufferedImage, 0, 0, preview.getWidth(), preview.getHeight(), null); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(thumbnail, "png", out); + + preview.setMediaType("image/png"); + preview.setData(out.toByteArray()); + preview.setSize(preview.getData().length); + image.setPreview(preview); + return image; + } + + public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception { + SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); + Document document = factory.createDocument(null, new ByteArrayInputStream(data)); + Integer width = null; + Integer height = null; + String strWidth = document.getDocumentElement().getAttribute("width"); + String strHeight = document.getDocumentElement().getAttribute("height"); + if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) { + try { + width = (int) Double.parseDouble(strWidth); + height = (int) Double.parseDouble(strHeight); + } catch (NumberFormatException ignored) {} // in case width and height are in %, mm, etc. + } + if (width == null || height == null) { + String viewBox = document.getDocumentElement().getAttribute("viewBox"); + if (StringUtils.isNotEmpty(viewBox)) { + String[] viewBoxValues = viewBox.split(" "); + if (viewBoxValues.length > 3) { + width = (int) Double.parseDouble(viewBoxValues[2]); + height = (int) Double.parseDouble(viewBoxValues[3]); + } + } + } + if (width == null) { + UserAgent agent = new UserAgentAdapter(); + DocumentLoader loader = new DocumentLoader(agent); + BridgeContext context = new BridgeContext(agent, loader); + context.setDynamic(true); + GVTBuilder builder = new GVTBuilder(); + GraphicsNode root = builder.build(context, document); + var bounds = root.getPrimitiveBounds(); + if (bounds != null) { + width = (int) bounds.getWidth(); + height = (int) bounds.getHeight(); + } + } + ProcessedImage image = new ProcessedImage(); + image.setMediaType(mediaType); + image.setWidth(width == null ? 0 : width); + image.setHeight(height == null ? 0 : height); + image.setData(data); + image.setSize(data.length); + + PNGTranscoder transcoder = new PNGTranscoder(); + transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_WIDTH, (float) thumbnailMaxDimension); + transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_HEIGHT, (float) thumbnailMaxDimension); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + transcoder.transcode(new TranscoderInput(new ByteArrayInputStream(data)), new TranscoderOutput(out)); + byte[] pngThumbnail = out.toByteArray(); + + ProcessedImage preview = new ProcessedImage(); + preview.setWidth(thumbnailMaxDimension); + preview.setHeight(thumbnailMaxDimension); + preview.setMediaType("image/png"); + preview.setData(pngThumbnail); + preview.setSize(pngThumbnail.length); + image.setPreview(preview); + return image; + } + + private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension) { + if (originalWidth <= maxDimension && originalHeight <= maxDimension) { + return new int[]{originalWidth, originalHeight}; + } + int thumbnailWidth; + int thumbnailHeight; + double aspectRatio = (double) originalWidth / originalHeight; + if (originalWidth > originalHeight) { + thumbnailWidth = maxDimension; + thumbnailHeight = (int) (maxDimension / aspectRatio); + } else { + thumbnailWidth = (int) (maxDimension * aspectRatio); + thumbnailHeight = maxDimension; + } + return new int[]{thumbnailWidth, thumbnailHeight}; + } + + @Data + public static class ProcessedImage { + private String mediaType; + private int width; + private int height; + private byte[] data; + private long size; + private ProcessedImage preview; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java b/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java new file mode 100644 index 0000000000..17454b4757 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class JsonNodeProcessingTask { + private final String path; + private final JsonNode node; + + public JsonNodeProcessingTask(String path, JsonNode node) { + this.path = path; + this.node = node; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java b/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java new file mode 100644 index 0000000000..c8703b6155 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.dao.dashboard.DashboardServiceImpl; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@Data +public class JsonPathProcessingTask { + private final String[] tokens; + private final Map variables; + private final JsonNode node; + + public JsonPathProcessingTask(String[] tokens, Map variables, JsonNode node) { + this.tokens = tokens; + this.variables = variables; + this.node = node; + } + + public boolean isLast() { + return tokens.length == 1; + } + + public String currentToken() { + return tokens[0]; + } + + public JsonPathProcessingTask next(JsonNode next) { + return new JsonPathProcessingTask( + Arrays.copyOfRange(tokens, 1, tokens.length), + variables, + next); + } + + public JsonPathProcessingTask next(JsonNode next, String key, String value) { + Map variables = new HashMap<>(this.variables); + variables.put(key, value); + return new JsonPathProcessingTask( + Arrays.copyOfRange(tokens, 1, tokens.length), + variables, + next); + } + + @Override + public String toString() { + return "JsonPathProcessingTask{" + + "tokens=" + Arrays.toString(tokens) + + ", variables=" + variables + + ", node=" + node.toString().substring(0, 20) + + '}'; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java index 96d93a34f1..23e25d12ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ImageContainerDao; import java.util.List; import java.util.UUID; @@ -33,7 +34,7 @@ import java.util.UUID; /** * The Interface WidgetTypeDao. */ -public interface WidgetTypeDao extends Dao, ExportableEntityDao { +public interface WidgetTypeDao extends Dao, ExportableEntityDao, ImageContainerDao { /** * Save or update widget type object @@ -108,4 +109,6 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD void removeWidgetTypeFromWidgetsBundle(UUID widgetsBundleId, UUID widgetTypeId); + PageData findAllWidgetTypesIds(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 2e347f2b62..4d9510d0b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -64,6 +65,9 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Autowired protected ApplicationEventPublisher eventPublisher; + @Autowired + protected ImageService imageService; + @Override public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing findWidgetTypeById [{}]", widgetTypeId); @@ -90,6 +94,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { log.trace("Executing saveWidgetType [{}]", widgetTypeDetails); widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId); try { + imageService.replaceBase64WithImageUrl(widgetTypeDetails); WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) .entityId(result.getId()).added(widgetTypeDetails.getId() == null).build()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index 9e0d70dcd8..939f9e43e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.widget; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -22,13 +23,14 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ImageContainerDao; import java.util.UUID; /** * The Interface WidgetsBundleDao. */ -public interface WidgetsBundleDao extends Dao, ExportableEntityDao { +public interface WidgetsBundleDao extends Dao, ExportableEntityDao, ImageContainerDao { /** * Save or update widgets bundle object @@ -83,5 +85,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa */ PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + PageData findAllWidgetsBundles(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 87a507873d..468bea6876 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -31,6 +31,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -59,6 +60,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Autowired protected ApplicationEventPublisher eventPublisher; + @Autowired + private ImageService imageService; + @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); @@ -71,6 +75,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { log.trace("Executing saveWidgetsBundle [{}]", widgetsBundle); widgetsBundleValidator.validate(widgetsBundle, WidgetsBundle::getTenantId); try { + imageService.replaceBase64WithImageUrl(widgetsBundle, "bundle"); WidgetsBundle result = widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) .entityId(result.getId()).added(widgetsBundle.getId() == null).build()); diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 68eede5ce4..472c0f601e 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -119,3 +119,5 @@ CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notificat CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ'; + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3b6d5966cd..35d1d49cda 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -714,8 +714,11 @@ CREATE TABLE IF NOT EXISTS resource ( resource_key varchar(255) NOT NULL, search_text varchar(255), file_name varchar(255) NOT NULL, - data varchar, + data bytea, etag varchar, + descriptor varchar, + preview bytea, + external_id uuid, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index ccddcb16bf..68e7174b19 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -20,16 +20,25 @@ import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.asset.AssetDao; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -37,6 +46,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -46,7 +56,13 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired AssetService assetService; @Autowired + AssetDao assetDao; + @Autowired CustomerService customerService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired + private PlatformTransactionManager platformTransactionManager; private IdComparator idComparator = new IdComparator<>(); @@ -75,6 +91,29 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testShouldNotPutInCacheRolledbackAssetProfile() { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setName(StringUtils.randomAlphabetic(10)); + assetProfile.setTenantId(tenantId); + + Asset asset = new Asset(); + asset.setName("My asset" + StringUtils.randomAlphabetic(15)); + asset.setType(assetProfile.getName()); + asset.setTenantId(tenantId); + + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + TransactionStatus status = platformTransactionManager.getTransaction(def); + try { + assetProfileService.saveAssetProfile(assetProfile); + assetService.saveAsset(asset); + } finally { + platformTransactionManager.rollback(status); + } + AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfile.getName()); + Assert.assertNull(assetProfileByName); + } + @Test public void testSaveAssetWithEmptyName() { Asset asset = new Asset(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 1073f20be4..3e404f8d1d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -22,6 +22,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -32,6 +35,8 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; @@ -72,6 +77,8 @@ public class DeviceServiceTest extends AbstractServiceTest { OtaPackageService otaPackageService; @Autowired TenantProfileService tenantProfileService; + @Autowired + private PlatformTransactionManager platformTransactionManager; private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; @@ -305,6 +312,28 @@ public class DeviceServiceTest extends AbstractServiceTest { }); } + @Test + public void testShouldNotPutInCacheRolledbackDeviceProfile() { + DeviceProfile deviceProfile = createDeviceProfile(tenantId, "New device Profile" + StringUtils.randomAlphabetic(5)); + + + Device device = new Device(); + device.setType(deviceProfile.getName()); + device.setTenantId(tenantId); + device.setName("My device"+ StringUtils.randomAlphabetic(5)); + + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + TransactionStatus status = platformTransactionManager.getTransaction(def); + try { + deviceProfileService.saveDeviceProfile(deviceProfile); + deviceService.saveDevice(device); + } finally { + platformTransactionManager.rollback(status); + } + DeviceProfile deviceProfileByName = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfile.getName()); + Assert.assertNull(deviceProfileByName); + } + @Test public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 85dc7cd7f0..03406a97f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -102,7 +102,7 @@ public class OtaPackageServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> createAndSaveFirmware(tenantId, "2")) .isInstanceOf(DataValidationException.class) - .hasMessageContaining("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE); + .hasMessageContaining("Ota packages total size exceeds the maximum of %d bytes", DATA_SIZE); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java index ce1a83514a..c7ae0aabb6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java @@ -73,6 +73,7 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -615,7 +616,7 @@ public class TenantServiceTest extends AbstractServiceTest { resource.setResourceType(ResourceType.LWM2M_MODEL); resource.setFileName("filename.txt"); resource.setResourceKey("Test resource key"); - resource.setData("Some super test data"); + resource.setData("Some super test data".getBytes(StandardCharsets.UTF_8)); return resourceService.saveResource(resource); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java index e6d46bb942..0a466d033c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java @@ -110,6 +110,7 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { widgetType.setName("WIDGET_TYPE_" + number); widgetType.setDescription("WIDGET_TYPE_DESCRIPTION" + number); widgetType.setFqn("FQN_" + number); + widgetType.setImage("/image/system/logo.png"); var descriptor = JacksonUtil.newObjectNode(); descriptor.put("type", number % 2 == 0 ? "latest" : "static"); widgetType.setDescriptor(descriptor); @@ -124,6 +125,7 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { widgetType.setName("WIDGET_TYPE_" + number); widgetType.setDescription("WIDGET_TYPE_DESCRIPTION" + number); widgetType.setFqn("FQN_" + number); + widgetType.setImage("/image/tenant/logo.png"); var descriptor = JacksonUtil.newObjectNode(); descriptor.put("type", number % 2 == 0 ? "latest" : "static"); widgetType.setDescriptor(descriptor); @@ -332,4 +334,25 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest { WidgetType widgetType = widgetTypeDao.findByTenantIdAndFqn(TenantId.SYS_TENANT_ID.getId(), "FQN_0"); assertEquals(result.getId(), widgetType.getId()); } + + @Test + public void testFindByTenantAndImageLink() { + var result = widgetTypeDao.findByImageLink("/image/system/logo.png", 5); + assertEquals(3, result.size()); + result = widgetTypeDao.findByImageLink("/image/system/logo2.png", 5); + assertEquals(0, result.size()); + } + + @Test + public void testFindByImageLink() { + TenantId tenantId = new TenantId(UUID.randomUUID()); + WidgetTypeDetails details = createAndSaveWidgetType(tenantId, 0, new String[]{"a"}); + details.setDescriptor(JacksonUtil.newObjectNode().put("bg", "/image/tenant/widget.png")); + widgetTypeDao.save(tenantId, details); + var result = widgetTypeDao.findByTenantAndImageLink(tenantId, "/image/tenant/widget.png", 3); + assertEquals(1, result.size()); + result = widgetTypeDao.findByTenantAndImageLink(tenantId, "/image/tenant/widget2.png", 3); + assertEquals(0, result.size()); + widgetTypeDao.removeById(tenantId, details.getUuidId()); + } } diff --git a/docker/docker-compose.hybrid.yml b/docker/docker-compose.hybrid.yml index 29cd65a127..7f952fe6eb 100644 --- a/docker/docker-compose.hybrid.yml +++ b/docker/docker-compose.hybrid.yml @@ -19,7 +19,7 @@ version: '3.0' services: postgres: restart: always - image: "postgres:12" + image: "postgres:15" ports: - "5432" environment: diff --git a/docker/docker-compose.postgres.yml b/docker/docker-compose.postgres.yml index 7f5f69a61f..218642317b 100644 --- a/docker/docker-compose.postgres.yml +++ b/docker/docker-compose.postgres.yml @@ -19,7 +19,7 @@ version: '3.0' services: postgres: restart: always - image: "postgres:12" + image: "postgres:15" ports: - "5432" environment: diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java index 0e62670f81..0176b14d54 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java @@ -21,4 +21,8 @@ public interface MonitoringTarget { UUID getDeviceId(); + String getBaseUrl(); + + boolean isCheckDomainIps(); + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java index 77d702f779..c5f843192c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java @@ -23,9 +23,8 @@ import java.util.List; @Data public abstract class TransportMonitoringConfig implements MonitoringConfig { - private int requestTimeoutMs; - private List targets; + private int requestTimeoutMs; public abstract TransportType getTransportType(); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java index 816f64fbce..e3277af0ff 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -25,6 +25,7 @@ public class TransportMonitoringTarget implements MonitoringTarget { private String baseUrl; private DeviceConfig device; // set manually during initialization + private boolean checkDomainIps; @Override public UUID getDeviceId() { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index 3370d42462..52e73a64cd 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -25,4 +25,8 @@ public class Latencies { return String.format("%sRequest", key); } + public static String wsUpdate(String key) { + return String.format("%sWsUpdate", key); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java index c64291e30f..4b17bf179f 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java @@ -15,53 +15,15 @@ */ package org.thingsboard.monitoring.data; -import com.google.common.util.concurrent.AtomicDouble; -import lombok.RequiredArgsConstructor; +import lombok.Data; -import java.util.concurrent.atomic.AtomicInteger; - -@RequiredArgsConstructor +@Data(staticConstructor = "of") public class Latency { - private final String key; - private final AtomicDouble latencySum = new AtomicDouble(); - private final AtomicInteger counter = new AtomicInteger(); - - public synchronized void report(double latencyInMs) { - latencySum.addAndGet(latencyInMs); - counter.incrementAndGet(); - } - - public synchronized double getAvg() { - return latencySum.get() / counter.get(); - } - - public boolean isNotEmpty() { - return counter.get() > 0; - } - - public synchronized void reset() { - latencySum.set(0.0); - counter.set(0); - } - - public String getKey() { - return key; - } - - public synchronized Latency snapshot() { - Latency snapshot = new Latency(key); - snapshot.latencySum.set(latencySum.get()); - snapshot.counter.set(counter.get()); - return snapshot; - } + private final double value; - @Override - public String toString() { - return "Latency{" + - "key='" + key + '\'' + - ", avgLatency=" + getAvg() + - '}'; + public String getFormattedValue() { + return String.format("%,.2f ms", value); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java index 939cb7440f..5744e40d41 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java @@ -21,11 +21,11 @@ import java.util.Collection; public class HighLatencyNotification implements Notification { - private final Collection latencies; + private final Collection highLatencies; private final int thresholdMs; - public HighLatencyNotification(Collection latencies, int thresholdMs) { - this.latencies = latencies; + public HighLatencyNotification(Collection highLatencies, int thresholdMs) { + this.highLatencies = highLatencies; this.thresholdMs = thresholdMs; } @@ -33,8 +33,8 @@ public class HighLatencyNotification implements Notification { public String getText() { StringBuilder text = new StringBuilder(); text.append("Some of the latencies are higher than ").append(thresholdMs).append(" ms:\n"); - latencies.forEach(latency -> { - text.append(String.format("[%s] %,.2f ms\n", latency.getKey(), latency.getAvg())); + highLatencies.forEach(latency -> { + text.append(String.format("[%s] %s\n", latency.getKey(), latency.getFormattedValue())); }); return text.toString(); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index affca1ce09..290fd307c6 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -15,6 +15,7 @@ */ package org.thingsboard.monitoring.service; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -30,13 +31,17 @@ import org.thingsboard.monitoring.util.TbStopWatch; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; @RequiredArgsConstructor @Slf4j public abstract class BaseHealthChecker { + @Getter protected final C config; + @Getter protected final T target; private Object info; @@ -48,6 +53,9 @@ public abstract class BaseHealthChecker> associates = new HashMap<>(); + public static final String TEST_TELEMETRY_KEY = "testData"; @PostConstruct @@ -84,6 +92,10 @@ public abstract class BaseHealthChecker { + healthChecker.check(wsClient); + }); } private void checkWsUpdate(WsClient wsClient, String testValue) { @@ -96,10 +108,9 @@ public abstract class BaseHealthChecker, T extends MonitoringTarget> { - @Autowired + @Autowired(required = false) private List configs; private final List> healthCheckers = new LinkedList<>(); private final List devices = new LinkedList<>(); @@ -54,18 +63,32 @@ public abstract class BaseMonitoringService, T ext @PostConstruct private void init() { + if (configs == null || configs.isEmpty()) { + return; + } tbClient.logIn(); configs.forEach(config -> { config.getTargets().forEach(target -> { - BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); - log.info("Initializing {}", healthChecker.getClass().getSimpleName()); - healthChecker.initialize(tbClient); - devices.add(target.getDeviceId()); + BaseHealthChecker healthChecker = initHealthChecker(target, config); healthCheckers.add(healthChecker); + + if (target.isCheckDomainIps()) { + getAssociatedUrls(target.getBaseUrl()).forEach(url -> { + healthChecker.getAssociates().put(url, initHealthChecker(createTarget(url), config)); + }); + } }); }); } + private BaseHealthChecker initHealthChecker(T target, C config) { + BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); + log.info("Initializing {} for {}", healthChecker.getClass().getSimpleName(), target.getBaseUrl()); + healthChecker.initialize(tbClient); + devices.add(target.getDeviceId()); + return healthChecker; + } + public final void runChecks() { if (healthCheckers.isEmpty()) { return; @@ -78,9 +101,8 @@ public abstract class BaseMonitoringService, T ext try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); - for (BaseHealthChecker healthChecker : healthCheckers) { - healthChecker.check(wsClient); + check(healthChecker, wsClient); } } reporter.reportLatencies(tbClient); @@ -94,8 +116,61 @@ public abstract class BaseMonitoringService, T ext } } + private void check(BaseHealthChecker healthChecker, WsClient wsClient) throws Exception { + healthChecker.check(wsClient); + + T target = healthChecker.getTarget(); + if (target.isCheckDomainIps()) { + Set associatedUrls = getAssociatedUrls(target.getBaseUrl()); + Map> associates = healthChecker.getAssociates(); + Set prevAssociatedUrls = new HashSet<>(associates.keySet()); + + boolean changed = false; + for (String url : associatedUrls) { + if (!prevAssociatedUrls.contains(url)) { + BaseHealthChecker associate = initHealthChecker(createTarget(url), healthChecker.getConfig()); + associates.put(url, associate); + changed = true; + } + } + for (String url : prevAssociatedUrls) { + if (!associatedUrls.contains(url)) { + stopHealthChecker(healthChecker); + associates.remove(url); + changed = true; + } + } + if (changed) { + log.info("Updated IPs for {}: {} (old list: {})", target.getBaseUrl(), associatedUrls, prevAssociatedUrls); + } + } + } + + @SneakyThrows + private Set getAssociatedUrls(String baseUrl) { + URI url = new URI(baseUrl); + return Arrays.stream(InetAddress.getAllByName(url.getHost())) + .map(InetAddress::getHostAddress) + .map(ip -> { + try { + return new URI(url.getScheme(), null, ip, url.getPort(), "", null, null).toString(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); + } + + private void stopHealthChecker(BaseHealthChecker healthChecker) throws Exception { + healthChecker.destroyClient(); + devices.remove(healthChecker.getTarget().getDeviceId()); + log.info("Stopped {} for {}", healthChecker.getClass().getSimpleName(), healthChecker.getTarget().getBaseUrl()); + } + protected abstract BaseHealthChecker createHealthChecker(C config, T target); + protected abstract T createTarget(String baseUrl); + protected abstract String getName(); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index f41454a76a..aabf04caee 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -63,25 +63,20 @@ public class MonitoringReporter { private String reportingAssetId; public void reportLatencies(TbClient tbClient) { - List latencies = this.latencies.values().stream() - .filter(Latency::isNotEmpty) - .map(latency -> { - Latency snapshot = latency.snapshot(); - latency.reset(); - return snapshot; - }) - .collect(Collectors.toList()); if (latencies.isEmpty()) { return; } - log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms") + log.debug("Latencies:\n{}", latencies.values().stream().map(latency -> latency.getKey() + ": " + latency.getFormattedValue()) .collect(Collectors.joining("\n")) + "\n"); - if (!latencyReportingEnabled) return; - if (latencies.stream().anyMatch(latency -> latency.getAvg() >= (double) latencyThresholdMs)) { - HighLatencyNotification highLatencyNotification = new HighLatencyNotification(latencies, latencyThresholdMs); + List highLatencies = latencies.values().stream() + .filter(latency -> latency.getValue() >= (double) latencyThresholdMs) + .collect(Collectors.toList()); + if (!highLatencies.isEmpty()) { + HighLatencyNotification highLatencyNotification = new HighLatencyNotification(highLatencies, latencyThresholdMs); notificationService.sendNotification(highLatencyNotification); + log.warn("{}", highLatencyNotification.getText()); } try { @@ -99,10 +94,11 @@ public class MonitoringReporter { } ObjectNode msg = JacksonUtil.newObjectNode(); - latencies.forEach(latency -> { - msg.set(latency.getKey(), new DoubleNode(latency.getAvg())); + latencies.values().forEach(latency -> { + msg.set(latency.getKey(), new DoubleNode(latency.getValue())); }); tbClient.saveEntityTelemetry(new AssetId(UUID.fromString(reportingAssetId)), "time", msg); + latencies.clear(); } catch (Exception e) { log.error("Failed to report latencies: {}", e.getMessage()); } @@ -112,7 +108,7 @@ public class MonitoringReporter { String latencyKey = key + "Latency"; double latencyInMs = (double) latencyInNanos / 1000_000; log.trace("Reporting latency [{}]: {} ms", key, latencyInMs); - latencies.computeIfAbsent(latencyKey, k -> new Latency(latencyKey)).report(latencyInMs); + latencies.put(latencyKey, Latency.of(latencyKey, latencyInMs)); } public void serviceFailure(Object serviceKey, Throwable error) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java index b3ce86e799..ca7c5a94da 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java @@ -33,6 +33,13 @@ public final class TransportsMonitoringService extends BaseMonitoringService 20;" + }, + "externalId": null + }, + { + "additionalInfo": { + "description": "", + "layoutX": 427, + "layoutY": 541 + }, + "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", + "name": "Test TBEL script", + "debugMode": false, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "scriptLang": "TBEL", + "jsScript": "return msg.temperature > 20;", + "tbelScript": "var a = \"a\";\nvar b = \"b\";\nreturn a.equals(\"a\") && b.equals(\"b\");" + }, + "externalId": null + }, + { + "additionalInfo": { + "description": "", + "layoutX": 40, + "layoutY": 252 + }, + "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", + "name": "Add arrival timestamp", + "debugMode": false, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "scriptLang": "TBEL", + "jsScript": "return {msg: msg, metadata: metadata, msgType: msgType};", + "tbelScript": "metadata.arrivalTs = Date.now();\nreturn {msg: msg, metadata: metadata, msgType: msgType};" + }, + "externalId": null + }, + { + "additionalInfo": { + "description": "", + "layoutX": 1467, + "layoutY": 267 + }, + "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", + "name": "Calculate additional latencies", + "debugMode": true, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "scriptLang": "TBEL", + "jsScript": "return {msg: msg, metadata: metadata, msgType: msgType};", + "tbelScript": "var arrivalLatency = metadata.arrivalTs - metadata.ts;\nvar processingTime = Date.now() - metadata.arrivalTs;\nmsg = {\n arrivalLatency: arrivalLatency,\n processingTime: processingTime\n};\nreturn {msg: msg, metadata: metadata, msgType: msgType};" + }, + "externalId": null + }, + { + "additionalInfo": { + "description": "", + "layoutX": 1438, + "layoutY": 403 + }, + "type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode", + "name": "To latencies asset", + "debugMode": false, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "originatorSource": "ENTITY", + "entityType": "ASSET", + "entityNamePattern": "[Monitoring] Latencies", + "relationsQuery": { + "direction": "FROM", + "maxLevel": 1, + "filters": [ + { + "relationType": "Contains", + "entityTypes": [] + } + ], + "fetchLastLevelOnly": false + } + }, + "externalId": null + }, + { + "additionalInfo": { + "description": null, + "layoutX": 1458, + "layoutY": 505 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": true, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "defaultTTL": 0 + }, + "externalId": null + }, + { + "additionalInfo": { + "description": "", + "layoutX": 928, + "layoutY": 266 + }, + "type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode", + "name": "Has testData", + "debugMode": false, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "messageNames": [ + "testData" + ], + "metadataNames": [], + "checkAllKeys": true + }, + "externalId": null + }, + { + "additionalInfo": { + "description": null, + "layoutX": 1203, + "layoutY": 327 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries with TTL", + "debugMode": true, + "singletonMode": false, + "configurationVersion": 0, + "configuration": { + "defaultTTL": 180, + "skipLatestPersistence": null, + "useServerTs": null + }, + "externalId": null + } + ], + "connections": [ + { + "fromIndex": 2, + "toIndex": 1, + "type": "Post attributes" + }, + { + "fromIndex": 2, + "toIndex": 3, + "type": "RPC Request from Device" + }, + { + "fromIndex": 2, + "toIndex": 4, + "type": "Other" + }, + { + "fromIndex": 2, + "toIndex": 5, + "type": "RPC Request to Device" + }, + { + "fromIndex": 2, + "toIndex": 16, + "type": "Post telemetry" + }, + { + "fromIndex": 6, + "toIndex": 2, + "type": "False" + }, + { + "fromIndex": 6, + "toIndex": 7, + "type": "True" + }, + { + "fromIndex": 7, + "toIndex": 2, + "type": "False" + }, + { + "fromIndex": 7, + "toIndex": 8, + "type": "True" + }, + { + "fromIndex": 8, + "toIndex": 2, + "type": "Success" + }, + { + "fromIndex": 9, + "toIndex": 10, + "type": "Success" + }, + { + "fromIndex": 10, + "toIndex": 11, + "type": "True" + }, + { + "fromIndex": 11, + "toIndex": 6, + "type": "True" + }, + { + "fromIndex": 12, + "toIndex": 9, + "type": "Success" + }, + { + "fromIndex": 13, + "toIndex": 14, + "type": "Success" + }, + { + "fromIndex": 14, + "toIndex": 15, + "type": "Success" + }, + { + "fromIndex": 16, + "toIndex": 0, + "type": "False" + }, + { + "fromIndex": 16, + "toIndex": 17, + "type": "True" + }, + { + "fromIndex": 17, + "toIndex": 13, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index 88979bff1a..57b9b39ec0 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -51,8 +51,10 @@ monitoring: # MQTT QoS qos: '${MQTT_QOS_LEVEL:1}' targets: - # MQTT transport base url, tcp://DOMAIN:1883 by default + # MQTT transport base url, tcp://DOMAIN:1883 by default - base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}' + # Whether to monitor IPs associated with the domain from base url + check_domain_ips: '${MQTT_TRANSPORT_CHECK_DOMAIN_IPS:false}' # To add more targets, use following environment variables: # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[2].base_url, etc. @@ -62,8 +64,10 @@ monitoring: # CoAP request timeout in milliseconds request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}' targets: - # CoAP transport base url, coap://DOMAIN by default + # CoAP transport base url, coap://DOMAIN by default - base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}' + # Whether to monitor IPs associated with the domain from base url + check_domain_ips: '${COAP_TRANSPORT_CHECK_DOMAIN_IPS:false}' # To add more targets, use following environment variables: # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[2].base_url, etc. @@ -73,8 +77,10 @@ monitoring: # HTTP request timeout in milliseconds request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}' targets: - # HTTP transport base url, http://DOMAIN by default + # HTTP transport base url, http://DOMAIN by default - base_url: '${HTTP_TRANSPORT_BASE_URL:http://${monitoring.domain}}' + # Whether to monitor IPs associated with the domain from base url + check_domain_ips: '${HTTP_TRANSPORT_CHECK_DOMAIN_IPS:false}' # To add more targets, use following environment variables: # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[2].base_url, etc. @@ -84,8 +90,10 @@ monitoring: # LwM2M request timeout in milliseconds request_timeout_ms: '${LWM2M_REQUEST_TIMEOUT_MS:4000}' targets: - # LwM2M transport base url, coap://DOMAIN:5685 by default + # LwM2M transport base url, coap://DOMAIN:5685 by default - base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}' + # Whether to monitor IPs associated with the domain from base url + check_domain_ips: '${LWM2M_TRANSPORT_CHECK_DOMAIN_IPS:false}' # To add more targets, use following environment variables: # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc. diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index fe467ec341..7d87351bef 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -253,12 +253,16 @@ public class ThingsBoardDbInstaller { .add(tbVcExecutorLogVolume) .add(resolveRedisComposeVolumeLog()); + if (IS_HYBRID_MODE) { + rmVolumesCommand.add(cassandraDataVolume); + } + dockerCompose.withCommand(rmVolumesCommand.toString()); } private String resolveRedisComposeVolumeLog() { if (IS_REDIS_CLUSTER) { - return IntStream.range(0, 6).mapToObj(i -> redisClusterDataVolume + "-" + i).collect(Collectors.joining()); + return IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + "-" + i).collect(Collectors.joining()); } if (IS_REDIS_SENTINEL) { return redisSentinelDataVolume + "-" + "master " + " " + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 5a7e9b631d..6ad50196b3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -26,6 +26,7 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -337,8 +338,12 @@ public class MqttClientTest extends AbstractContainerTest { MqttClient mqttClient = getMqttClient(deviceCredentials, listener); testRestClient.deleteDeviceIfExists(device.getId()); - TimeUnit.SECONDS.sleep(3 * timeoutMultiplier); - assertThat(mqttClient.isConnected()).isFalse(); + + Awaitility + .await() + .alias("Check device connection.") + .atMost(10, TimeUnit.SECONDS) + .until(() -> !mqttClient.isConnected()); } @Test diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 2f23aec399..0a0c36ed88 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -16,7 +16,7 @@ FROM thingsboard/openjdk11:bullseye-slim -ENV PG_MAJOR=12 +ENV PG_MAJOR=15 ENV DATA_FOLDER=/data diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 4ffa1ab702..8c363b019c 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -172,6 +172,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" #in bytes # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Core subscribers, messages which will commit after ackDeadlineInSec period can be consumed again core: "${TB_QUEUE_PUBSUB_CORE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/pom.xml b/pom.xml index 7fd3769704..ed62452870 100755 --- a/pom.xml +++ b/pom.xml @@ -153,6 +153,8 @@ 1.12.1 6.4.2 1.34.1 + 1.17 + 2.19.0 @@ -893,6 +895,11 @@ version-control ${project.version} + + org.thingsboard.common + proto + ${project.version} + org.thingsboard.common cache @@ -2029,6 +2036,21 @@ google-oauth-client ${google-oauth-client.version} + + org.apache.xmlgraphics + batik-transcoder + ${apache-xmlgraphics.version} + + + org.apache.xmlgraphics + batik-codec + ${apache-xmlgraphics.version} + + + com.drewnoakes + metadata-extractor + ${drewnoakes-metadata-extractor.version} + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index e645110a9d..40ed2c378a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api; import io.netty.channel.EventLoopGroup; +import org.thingsboard.common.util.ExecutorProvider; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -318,6 +319,8 @@ public interface TbContext { ListeningExecutor getNotificationExecutor(); + ExecutorProvider getPubSubRuleNodeExecutorProvider(); + MailService getMailService(boolean isSystem); SmsService getSmsService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index c103dd4006..af4c7bc0c2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import javax.annotation.Nullable; import java.util.ArrayList; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index c55113783c..3067947445 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -20,6 +20,7 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.FixedExecutorProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.pubsub.v1.Publisher; @@ -68,7 +69,7 @@ public class TbPubSubNode extends TbAbstractExternalNode { super.init(ctx); this.config = TbNodeUtils.convert(configuration, TbPubSubNodeConfiguration.class); try { - this.pubSubClient = initPubSubClient(); + this.pubSubClient = initPubSubClient(ctx); } catch (Exception e) { throw new TbNodeException(e); } @@ -128,7 +129,7 @@ public class TbPubSubNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } - private Publisher initPubSubClient() throws IOException { + private Publisher initPubSubClient(TbContext ctx) throws IOException { ProjectTopicName topicName = ProjectTopicName.of(config.getProjectId(), config.getTopicName()); ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( @@ -148,6 +149,7 @@ public class TbPubSubNode extends TbAbstractExternalNode { return Publisher.newBuilder(topicName) .setCredentialsProvider(credProvider) .setRetrySettings(retrySettings) + .setExecutorProvider(FixedExecutorProvider.create(ctx.getPubSubRuleNodeExecutorProvider().getExecutor())) .build(); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index f31896a8c9..6bb37438a1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.msg.tools.SchedulerUtils; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import java.time.Instant; import java.time.ZoneId; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 2aeb4eedf5..b38fd29af7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import java.util.ArrayList; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 9bbee45477..06498241fb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import java.util.ArrayList; import java.util.List; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 799089d4a8..8762839c07 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.adaptor.JsonConverter; import java.util.ArrayList; import java.util.List; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java index 8d9a7102d0..f413be43c1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java @@ -89,7 +89,9 @@ public class TbCopyKeysNode implements TbNode { String keyData = entry.getKey(); if (checkKey(keyData)) { msgChanged = true; - metaData.putValue(keyData, JacksonUtil.toString(entry.getValue())); + String value = entry.getValue().isTextual() ? + entry.getValue().asText() : JacksonUtil.toString(entry.getValue()); + metaData.putValue(keyData, value); } } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java index e0ab34e35b..0fb9589e90 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java @@ -96,10 +96,12 @@ public class TbCopyKeysNodeTest { @Test void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception { config.setFromMetadata(false); + config.setKeys(Set.of(".*Key$")); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); - String data = "{\"DigitData\":22.5,\"TempDataValue\":10.5}"; + String data = "{\"nullKey\":null,\"stringKey\":\"value1\",\"booleanKey\":true,\"doubleKey\":42.0,\"longKey\":73," + + "\"jsonKey\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; node.onMsg(ctx, getTbMsg(deviceId, data)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -110,8 +112,13 @@ public class TbCopyKeysNodeTest { assertThat(newMsg).isNotNull(); Map metaDataMap = newMsg.getMetaData().getData(); - assertThat(metaDataMap.containsKey("DigitData")).isEqualTo(true); - assertThat(metaDataMap.containsKey("TempDataValue")).isEqualTo(true); + assertThat(metaDataMap.get("nullKey")).isEqualTo("null"); + assertThat(metaDataMap.get("stringKey")).isEqualTo("value1"); + assertThat(metaDataMap.get("booleanKey")).isEqualTo("true"); + assertThat(metaDataMap.get("doubleKey")).isEqualTo("42.0"); + assertThat(metaDataMap.get("longKey")).isEqualTo("73"); + assertThat(metaDataMap.get("jsonKey")) + .isEqualTo("{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}"); } @Test diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 437f551690..a45b41803e 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: @@ -295,6 +299,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consumed again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 8d4d6fadf0..81fdf414bb 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -79,6 +79,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: @@ -278,6 +282,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per a consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consume again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 8fd1c11e6e..f68f158dc1 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: @@ -374,6 +378,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consumed again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 6c51d18ab7..16c2c019df 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: @@ -311,6 +315,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consumed again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index eeb6807714..2b6cc17d9b 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: @@ -264,6 +268,8 @@ queue: max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" # Number of messages per consumer max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + # Thread pool size for pubsub queue executor provider. If not set - default pubsub executor provider value will be used (5 * number of available processors) + executor_thread_pool_size: "${TB_QUEUE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" queue-properties: # Pub/Sub properties for Rule Engine subscribers, messages which will commit after ackDeadlineInSec period can be consumed again rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" diff --git a/ui-ngx/src/app/core/http/asset-profile.service.ts b/ui-ngx/src/app/core/http/asset-profile.service.ts index 0434f5f960..029e1afe5c 100644 --- a/ui-ngx/src/app/core/http/asset-profile.service.ts +++ b/ui-ngx/src/app/core/http/asset-profile.service.ts @@ -40,6 +40,10 @@ export class AssetProfileService { return this.http.get(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); } + public exportAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/assetProfile/${assetProfileId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); + } + public saveAssetProfile(assetProfile: AssetProfile, config?: RequestConfig): Observable { return this.http.post('/api/assetProfile', assetProfile, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index 6751e0e38f..3c45a9e95b 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -20,9 +20,9 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; -import { EntitySubtype } from '@app/shared/models/entity-type.models'; -import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models'; -import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; +import { EntitySubtype } from '@shared/models/entity-type.models'; +import { Asset, AssetInfo, AssetSearchQuery } from '@shared/models/asset.models'; +import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; @Injectable({ providedIn: 'root' diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 706967b70d..fc15e7aa74 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -71,6 +71,10 @@ export class DashboardService { return this.http.get(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); } + public exportDashboard(dashboardId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/dashboard/${dashboardId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); + } + public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable { return this.http.get(`/api/dashboard/info/${dashboardId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 047374783e..fd7753a6ab 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -53,6 +53,10 @@ export class DeviceProfileService { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } + public exportDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfile/${deviceProfileId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); + } + public getLwm2mObjects(sortOrder: SortOrder, objectIds?: string[], searchText?: string, config?: RequestConfig): Observable> { let url = `/api/resource/lwm2m/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 801bdeb8c3..fbabbaadfa 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -30,10 +30,10 @@ import { DeviceSearchQuery, PublishLaunchCommand, PublishTelemetryCommand -} from '@app/shared/models/device.models'; -import { EntitySubtype } from '@app/shared/models/entity-type.models'; +} from '@shared/models/device.models'; +import { EntitySubtype } from '@shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; -import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; +import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; @Injectable({ diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts index aeb4f7d16b..4ddcc69458 100644 --- a/ui-ngx/src/app/core/http/edge.service.ts +++ b/ui-ngx/src/app/core/http/edge.service.ts @@ -23,7 +23,7 @@ import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { Edge, EdgeEvent, EdgeInfo, EdgeInstallInstructions, EdgeSearchQuery } from '@shared/models/edge.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; +import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; @Injectable({ providedIn: 'root' diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 8bc8bfe3f2..968329d36a 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -95,6 +95,7 @@ import { TenantProfileService } from '@core/http/tenant-profile.service'; import { NotificationType } from '@shared/models/notification.models'; import { UserId } from '@shared/models/id/user-id'; import { AlarmService } from '@core/http/alarm.service'; +import { ResourceService } from '@core/http/resource.service'; @Injectable({ providedIn: 'root' @@ -123,7 +124,8 @@ export class EntityService { private utils: UtilsService, private queueService: QueueService, private notificationService: NotificationService, - private alarmService: AlarmService + private alarmService: AlarmService, + private resourceService: ResourceService ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -438,6 +440,10 @@ export class EntityService { pageLink.sortOrder.property = 'name'; entitiesObservable = this.notificationService.getNotificationRules(pageLink, config); break; + case EntityType.TB_RESOURCE: + pageLink.sortOrder.property = 'title'; + entitiesObservable = this.resourceService.getTenantResources(pageLink, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts new file mode 100644 index 0000000000..3fcba170f4 --- /dev/null +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; +import { Observable, of } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { + NO_IMAGE_DATA_URI, + ImageResourceInfo, + imageResourceType, + ImageResourceType, + IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix +} from '@shared/models/resource.models'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { blobToBase64 } from '@core/utils'; + +@Injectable({ + providedIn: 'root' +}) +export class ImageService { + constructor( + private http: HttpClient, + private sanitizer: DomSanitizer + ) { + } + + public uploadImage(file: File, title: string, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', file); + formData.append('title', title); + return this.http.post('/api/image', formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public updateImage(type: ImageResourceType, key: string, file: File, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', file); + return this.http.put(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public updateImageInfo(imageInfo: ImageResourceInfo, config?: RequestConfig): Observable { + const type = imageResourceType(imageInfo); + const key = encodeURIComponent(imageInfo.resourceKey); + return this.http.put(`${IMAGES_URL_PREFIX}/${type}/${key}/info`, + imageInfo, defaultHttpOptionsFromConfig(config)); + } + + public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable> { + return this.http.get>( + `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`, + defaultHttpOptionsFromConfig(config)); + } + + public getImageInfo(type: ImageResourceType, key: string, config?: RequestConfig): Observable { + return this.http.get(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}/info`, + defaultHttpOptionsFromConfig(config)); + } + + public getImageDataUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable { + const parts = imageUrl.split('/'); + const key = parts[parts.length - 1]; + parts[parts.length - 1] = encodeURIComponent(key); + const encodedUrl = parts.join('/'); + const imageLink = preview ? (encodedUrl + '/preview') : encodedUrl; + const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); + return this.http + .get(imageLink, {...options, ...{ responseType: 'blob' } }).pipe( + switchMap(val => blobToBase64(val).pipe( + map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl)) + )), + catchError(() => of(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl))) + ); + } + + public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable { + imageUrl = removeTbImagePrefix(imageUrl); + if (isImageResourceUrl(imageUrl)) { + return this.getImageDataUrl(imageUrl, preview, asString, emptyUrl); + } else { + return of(asString ? imageUrl : this.sanitizer.bypassSecurityTrustUrl(imageUrl)); + } + } + + public downloadImage(type: ImageResourceType, key: string): Observable { + return this.http.get(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`, { + 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 url = URL.createObjectURL(blob); + linkElement.setAttribute('href', url); + linkElement.setAttribute('download', filename); + const clickEvent = new MouseEvent('click', + { + view: window, + bubbles: true, + cancelable: false + } + ); + linkElement.dispatchEvent(clickEvent); + return null; + } catch (e) { + throw e; + } + }) + ); + } + + public deleteImage(type: ImageResourceType, key: string, force = false, config?: RequestConfig) { + return this.http.delete(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}?force=${force}`, defaultHttpOptionsFromConfig(config)); + } + + public exportImage(type: ImageResourceType, key: string, config?: RequestConfig): Observable { + return this.http.get(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}/export`, + defaultHttpOptionsFromConfig(config)); + } + + public importImage(imageData: ImageExportData, config?: RequestConfig): Observable { + return this.http.put('/api/image/import', + imageData, 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 f7cb8441d1..d6820f93cb 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -42,6 +42,10 @@ export class ResourceService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } + public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + public getResource(resourceId: string, config?: RequestConfig): Observable { return this.http.get(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 6905a0f594..16db878fdf 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -98,6 +98,11 @@ export class WidgetService { return this.http.get(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptionsFromConfig(config)); } + public exportWidgetsBundle(widgetsBundleId: string, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetsBundle/${widgetsBundleId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); + } + public saveWidgetsBundle(widgetsBundle: WidgetsBundle, config?: RequestConfig): Observable { return this.http.post('/api/widgetsBundle', widgetsBundle, @@ -137,9 +142,9 @@ export class WidgetService { defaultHttpOptionsFromConfig(config)); } - public getBundleWidgetTypesDetails(widgetsBundleId: string, + public exportBundleWidgetTypesDetails(widgetsBundleId: string, config?: RequestConfig): Observable> { - return this.http.get>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}`, + return this.http.get>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); } @@ -204,6 +209,12 @@ export class WidgetService { defaultHttpOptionsFromConfig(config)); } + public exportWidgetType(widgetTypeId: string, + config?: RequestConfig): Observable { + return this.http.get(`/api/widgetType/${widgetTypeId}?inlineImages=true`, + defaultHttpOptionsFromConfig(config)); + } + public getWidgetTypeInfoById(widgetTypeId: string, config?: RequestConfig): Observable { return this.http.get(`/api/widgetTypeInfo/${widgetTypeId}`, diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index d48b0619ca..924d741cf6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -143,6 +143,13 @@ export class MenuService { } ] }, + { + id: 'images', + name: 'image.gallery', + type: 'link', + path: '/resources/images', + icon: 'filter' + }, { id: 'resources_library', name: 'resource.resources-library', @@ -527,6 +534,13 @@ export class MenuService { } ] }, + { + id: 'images', + name: 'image.gallery', + type: 'link', + path: '/resources/images', + icon: 'filter' + }, { id: 'resources_library', name: 'resource.resources-library', diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b810689b69..feeb5de02f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -15,7 +15,7 @@ /// import _ from 'lodash'; -import { Observable, Subject } from 'rxjs'; +import { from, Observable, Subject } from 'rxjs'; import { finalize, share } from 'rxjs/operators'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; @@ -209,6 +209,13 @@ export function stringToBase64(value: string): string { })); } +export const blobToBase64 = (blob: Blob): Observable => from(new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsDataURL(blob); + } + )); + const scrollRegex = /(auto|scroll)/; function parentNodes(node: Node, nodes: Node[]): Node[] { diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index d93a84ea51..41e10d5de3 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -95,6 +95,7 @@ import * as SelectableColumnsPipe from '@shared/pipe/selectable-columns.pipe'; import * as ShortNumberPipe from '@shared/pipe/short-number.pipe'; import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; import * as TruncatePipe from '@shared/pipe/truncate.pipe'; +import * as ImagePipe from '@shared/pipe/image.pipe'; import * as coercion from '@shared/decorators/coercion'; import * as enumerable from '@shared/decorators/enumerable'; @@ -187,6 +188,9 @@ import * as UnitInputComponent from '@shared/components/unit-input.component'; import * as MaterialIconsComponent from '@shared/components/material-icons.component'; import * as TbIconComponent from '@shared/components/icon.component'; import * as HintTooltipIconComponent from '@shared/components/hint-tooltip-icon.component'; +import * as ScrollGridComponent from '@shared/components/grid/scroll-grid.component'; +import * as GalleryImageInputComponent from '@shared/components/image/gallery-image-input.component'; +import * as MultipleGalleryImageInputComponent from '@shared/components/image/multiple-gallery-image-input.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; @@ -227,10 +231,10 @@ import * as CustomActionPrettyEditorComponent from '@home/components/widget/acti import * as MobileActionEditorComponent from '@home/components/widget/action/mobile-action-editor.component'; import * as CustomDialogService from '@home/components/widget/dialog/custom-dialog.service'; import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component'; -import * as ImportDialogComponent from '@home/components/import-export/import-dialog.component'; +import * as ImportDialogComponent from '@shared/import-export/import-dialog.component'; import * as AddWidgetToDashboardDialogComponent from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; -import * as ImportDialogCsvComponent from '@home/components/import-export/import-dialog-csv.component'; -import * as TableColumnsAssignmentComponent from '@home/components/import-export/table-columns-assignment.component'; +import * as ImportDialogCsvComponent from '@shared/import-export/import-dialog-csv.component'; +import * as TableColumnsAssignmentComponent from '@shared/import-export/table-columns-assignment.component'; import * as EventContentDialogComponent from '@home/components/event/event-content-dialog.component'; import * as SharedHomeComponentsModule from '@home/components/shared-home-components.module'; import * as SelectTargetLayoutDialogComponent from '@home/components/dashboard/select-target-layout-dialog.component'; @@ -401,11 +405,16 @@ class ModulesMap implements IModulesMap { '@shared/pipe/short-number.pipe': ShortNumberPipe, '@shared/pipe/tbJson.pipe': TbJsonPipe, '@shared/pipe/truncate.pipe': TruncatePipe, + '@shared/pipe/image.pipe': ImagePipe, '@shared/decorators/coercion': coercion, '@shared/decorators/enumerable': enumerable, '@shared/decorators/tb-inject': TbInject, + '@shared/import-export/import-dialog.component': ImportDialogComponent, + '@shared/import-export/import-dialog-csv.component': ImportDialogCsvComponent, + '@shared/import-export/table-columns-assignment.component': TableColumnsAssignmentComponent, + '@shared/components/footer.component': FooterComponent, '@shared/components/logo.component': LogoComponent, '@shared/components/footer-fab-buttons.component': FooterFabButtonsComponent, @@ -494,6 +503,9 @@ class ModulesMap implements IModulesMap { '@shared/components/material-icons.component': MaterialIconsComponent, '@shared/components/icon.component': TbIconComponent, '@shared/components/hint-tooltip-icon.component': HintTooltipIconComponent, + '@shared/components/grid/scroll-grid.component': ScrollGridComponent, + '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, + '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent, @@ -534,10 +546,7 @@ class ModulesMap implements IModulesMap { '@home/components/widget/action/mobile-action-editor.component': MobileActionEditorComponent, '@home/components/widget/dialog/custom-dialog.service': CustomDialogService, '@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent, - '@home/components/import-export/import-dialog.component': ImportDialogComponent, '@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent, - '@home/components/import-export/import-dialog-csv.component': ImportDialogCsvComponent, - '@home/components/import-export/table-columns-assignment.component': TableColumnsAssignmentComponent, '@home/components/event/event-content-dialog.component': EventContentDialogComponent, '@home/components/shared-home-components.module': SharedHomeComponentsModule, '@home/components/dashboard/select-target-layout-dialog.component': SelectTargetLayoutDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts index 5ba3c39a81..623256e1e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -31,7 +31,7 @@ import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models import { UtilsService } from '@core/services/utils.service'; import { EntityType } from '@shared/models/entity-type.models'; import { DatePipe } from '@angular/common'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; interface AlarmCommentsDisplayData { commentId?: string, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html index 9019161c29..8939d885d3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html @@ -40,8 +40,8 @@
-
{{ 'dashboard.no-image' | translate }}
- +
{{ 'dashboard.no-image' | translate }}
+
Right % @@ -62,9 +62,9 @@
- - +
-
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 6f6e39c52c..49ed46d440 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 @@ -119,7 +119,7 @@ import { ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogData } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { AuthState } from '@app/core/auth/auth.models'; import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component'; import { Filters } from '@shared/models/query/query.models'; @@ -145,7 +145,7 @@ import { IAliasController } from '@core/api/widget-api.models'; import { MatButton } from '@angular/material/button'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { TbPopoverService } from '@shared/components/popover.service'; -import { tap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ResizeObserver } from '@juggle/resize-observer'; @@ -668,12 +668,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } - public get dashboardLogo(): SafeUrl { - if (!this.dashboardLogoCache) { - const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo; - this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo); - } - return this.dashboardLogoCache; + public get dashboardLogo(): string { + return this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo; } public showRightLayoutSwitch(): boolean { @@ -923,6 +919,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC $event.stopPropagation(); } this.importExport.importWidget(this.dashboard, this.dashboardCtx.state, + this.editMissingAliases.bind(this), this.selectTargetLayout.bind(this), this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe( (importData) => { if (importData) { @@ -939,6 +936,30 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC ); } + private editMissingAliases(widgets: Array, isSingleWidget: boolean, + customTitle: string, missingEntityAliases: EntityAliases): Observable { + return this.dialog.open(EntityAliasesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityAliases: missingEntityAliases, + widgets, + customTitle, + isSingleWidget, + disableAdd: true + } + }).afterClosed().pipe( + map((updatedEntityAliases) => { + if (updatedEntityAliases) { + return updatedEntityAliases; + } else { + throw new Error('Unable to resolve missing entity aliases!'); + } + } + )); + } + public currentDashboardIdChanged(dashboardId: string) { if (!this.widgetEditMode) { this.dashboardCtx.stateController.cleanupPreservedStates(); 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 2090e5f42c..d526f180e0 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 @@ -55,10 +55,10 @@ {{ 'dashboard.display-dashboard-logo' | translate }} - - +
dashboard.toolbar-settings @@ -144,10 +144,10 @@ openOnInput formControlName="backgroundColor"> - - + dashboard.background-size-mode diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index 4431820808..5bb677e814 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -27,7 +27,7 @@
- {{ item.title }} + {{ item.title }}
@@ -74,7 +74,7 @@
- {{ item.title }} + {{ item.title }}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts index 7e8f31578b..0e73c35724 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts @@ -28,11 +28,11 @@ import { } from '@shared/models/widget.models'; import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators'; import { BehaviorSubject, combineLatest } from 'rxjs'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { isDefinedAndNotNull, isObject } from '@core/utils'; +import { isObject } from '@core/utils'; import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; -import { GridEntitiesFetchFunction, ScrollGridColumns } from '@home/models/datasource/scroll-grid-datasource'; +import { GridEntitiesFetchFunction, ScrollGridColumns } from '@shared/components/grid/scroll-grid-datasource'; +import { ItemSizeStrategy } from '@shared/components/grid/scroll-grid.component'; type selectWidgetMode = 'bundles' | 'allWidgets'; @@ -149,6 +149,11 @@ export class DashboardWidgetSelectComponent implements OnInit { } }; + gridWidgetsItemSizeStrategy: ItemSizeStrategy = { + defaultItemSize: 160, + itemSizeFunction: itemWidth => (itemWidth - 24) * 0.8 + 76 + }; + widgetBundlesFetchFunction: GridEntitiesFetchFunction; allWidgetsFetchFunction: GridEntitiesFetchFunction; widgetsFetchFunction: GridEntitiesFetchFunction; @@ -158,8 +163,7 @@ export class DashboardWidgetSelectComponent implements OnInit { widgetsFilter: BundleWidgetsFilter = {search: '', filter: null, deprecatedFilter: DeprecatedFilter.ACTUAL, widgetsBundleId: null}; constructor(private widgetsService: WidgetService, - private cd: ChangeDetectorRef, - private sanitizer: DomSanitizer) { + private cd: ChangeDetectorRef) { this.widgetBundlesFetchFunction = (pageSize, page, filter) => { const pageLink = new PageLink(pageSize, page, filter, { @@ -241,13 +245,6 @@ export class DashboardWidgetSelectComponent implements OnInit { } } - getPreviewImage(imageUrl: string | null): SafeUrl | string { - if (isDefinedAndNotNull(imageUrl)) { - return this.sanitizer.bypassSecurityTrustUrl(imageUrl); - } - return '/assets/widget-preview-empty.svg'; - } - isObject(value: any): boolean { return isObject(value); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html index f724f81f04..d43f2af10f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html @@ -23,7 +23,7 @@
; hotKeys: Hotkey[] = []; @@ -92,6 +94,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo constructor(protected store: Store, private translate: TranslateService, private itembuffer: ItemBufferService, + private imagePipe: ImagePipe, private sanitizer: DomSanitizer) { super(store); this.initHotKeys(); @@ -188,8 +191,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo 'background-attachment': 'scroll', 'background-size': this.layoutCtx.gridSettings.backgroundSizeMode || '100%', 'background-position': '0% 0%'}; - this.backgroundImage = this.layoutCtx.gridSettings.backgroundImageUrl ? - this.sanitizer.bypassSecurityTrustStyle('url(' + this.layoutCtx.gridSettings.backgroundImageUrl + ')') : 'none'; + this.backgroundImage$ = this.layoutCtx.gridSettings.backgroundImageUrl ? + this.imagePipe.transform(this.layoutCtx.gridSettings.backgroundImageUrl, {asString: true, ignoreLoadingImage: true}).pipe( + map((imageUrl) => this.sanitizer.bypassSecurityTrustStyle('url(' + imageUrl + ')')) + ) : of('none'); } reload() { diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index 748723197f..bbb0e2006e 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -56,7 +56,7 @@
+
+ + tenant-profile.maximum-resource-size + + + {{ 'tenant-profile.maximum-resource-size-required' | translate}} + + + {{ 'tenant-profile.maximum-resource-size-range' | translate}} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index 5b48e02e07..7718cd4b2f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -67,6 +67,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA maxRuleChains: [null, [Validators.required, Validators.min(0)]], maxResourcesInBytes: [null, [Validators.required, Validators.min(0)]], maxOtaPackagesInBytes: [null, [Validators.required, Validators.min(0)]], + maxResourceSize: [null, [Validators.required, Validators.min(0)]], transportTenantMsgRateLimit: [null, []], transportTenantTelemetryMsgRateLimit: [null, []], transportTenantTelemetryDataPointsRateLimit: [null, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 5bb6d9ea52..5e95d162cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -85,6 +85,9 @@ import { import { DoughnutBasicConfigComponent } from '@home/components/widget/config/basic/chart/doughnut-basic-config.component'; +import { + RangeChartBasicConfigComponent +} from '@home/components/widget/config/basic/chart/range-chart-basic-config.component'; @NgModule({ declarations: [ @@ -111,7 +114,8 @@ import { ThermometerScaleGaugeBasicConfigComponent, CompassGaugeBasicConfigComponent, LiquidLevelCardBasicConfigComponent, - DoughnutBasicConfigComponent + DoughnutBasicConfigComponent, + RangeChartBasicConfigComponent ], imports: [ CommonModule, @@ -142,7 +146,8 @@ import { ThermometerScaleGaugeBasicConfigComponent, CompassGaugeBasicConfigComponent, LiquidLevelCardBasicConfigComponent, - DoughnutBasicConfigComponent + DoughnutBasicConfigComponent, + RangeChartBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -167,5 +172,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html new file mode 100644 index 0000000000..66b4054df1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -0,0 +1,222 @@ + + + + + + +
+
widget-config.appearance
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-chart-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
+
widgets.range-chart.chart
+
+ + {{ 'widgets.range-chart.data-zoom' | translate }} + +
+
+
{{ 'widgets.range-chart.range-colors' | translate }}
+ + +
+
+
{{ 'widgets.range-chart.out-of-range-color' | translate }}
+ + +
+
+ + {{ 'widgets.range-chart.fill-area' | translate }} + +
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
+
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + +
+
{{ 'legend.position' | translate }}
+ + + + {{ legendPositionTranslationMap.get(pos) | translate }} + + + +
+
+
{{ 'widgets.range-chart.legend-label' | translate }}
+
+ + + + +
+
+
+
+
+
+ + + + + {{ 'widgets.range-chart.tooltip' | translate }} + + + + +
+
{{ 'widgets.range-chart.tooltip-value' | translate }}
+
+ + + + +
+
+
+ + {{ 'widgets.range-chart.tooltip-date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.range-chart.tooltip-background-color' | translate }}
+ + +
+
+
{{ 'widgets.range-chart.tooltip-background-blur' | translate }}
+ + +
px
+
+
+
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts new file mode 100644 index 0000000000..5b22637296 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -0,0 +1,269 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Injector } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { DataKey, legendPositions, legendPositionTranslationMap, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { + cssSizeToStrSize, + DateFormatProcessor, + DateFormatSettings, + resolveCssSize +} from '@shared/models/widget-settings.models'; +import { + rangeChartDefaultSettings, + RangeChartWidgetSettings +} from '@home/components/widget/lib/chart/range-chart-widget.models'; + +@Component({ + selector: 'tb-range-chart-basic-config', + templateUrl: './range-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { + + legendPositions = legendPositions; + + legendPositionTranslationMap = legendPositionTranslationMap; + + rangeChartWidgetConfigForm: UntypedFormGroup; + + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); + + tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.rangeChartWidgetConfigForm; + } + + protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { + return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: RangeChartWidgetSettings = {...rangeChartDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.rangeChartWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + dataZoom: [settings.dataZoom, []], + rangeColors: [settings.rangeColors, []], + outOfRangeColor: [settings.outOfRangeColor, []], + fillArea: [settings.fillArea, []], + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + + showLegend: [settings.showLegend, []], + legendPosition: [settings.legendPosition, []], + legendLabelFont: [settings.legendLabelFont, []], + legendLabelColor: [settings.legendLabelColor, []], + + showTooltip: [settings.showTooltip, []], + tooltipValueFont: [settings.tooltipValueFont, []], + tooltipValueColor: [settings.tooltipValueColor, []], + tooltipShowDate: [settings.tooltipShowDate, []], + tooltipDateFormat: [settings.tooltipDateFormat, []], + tooltipDateFont: [settings.tooltipDateFont, []], + tooltipDateColor: [settings.tooltipDateColor, []], + tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], + tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.dataZoom = config.dataZoom; + this.widgetConfig.config.settings.rangeColors = config.rangeColors; + this.widgetConfig.config.settings.outOfRangeColor = config.outOfRangeColor; + this.widgetConfig.config.settings.fillArea = config.fillArea; + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + + this.widgetConfig.config.settings.showLegend = config.showLegend; + this.widgetConfig.config.settings.legendPosition = config.legendPosition; + this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; + this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; + + this.widgetConfig.config.settings.showTooltip = config.showTooltip; + this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont; + this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor; + this.widgetConfig.config.settings.tooltipShowDate = config.tooltipShowDate; + this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat; + this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont; + this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor; + this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; + this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.rangeChartWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.rangeChartWidgetConfigForm.get('showIcon').value; + const showLegend: boolean = this.rangeChartWidgetConfigForm.get('showLegend').value; + const showTooltip: boolean = this.rangeChartWidgetConfigForm.get('showTooltip').value; + const tooltipShowDate: boolean = this.rangeChartWidgetConfigForm.get('tooltipShowDate').value; + + if (showTitle) { + this.rangeChartWidgetConfigForm.get('title').enable(); + this.rangeChartWidgetConfigForm.get('titleFont').enable(); + this.rangeChartWidgetConfigForm.get('titleColor').enable(); + this.rangeChartWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.rangeChartWidgetConfigForm.get('iconSize').enable(); + this.rangeChartWidgetConfigForm.get('iconSizeUnit').enable(); + this.rangeChartWidgetConfigForm.get('icon').enable(); + this.rangeChartWidgetConfigForm.get('iconColor').enable(); + } else { + this.rangeChartWidgetConfigForm.get('iconSize').disable(); + this.rangeChartWidgetConfigForm.get('iconSizeUnit').disable(); + this.rangeChartWidgetConfigForm.get('icon').disable(); + this.rangeChartWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.rangeChartWidgetConfigForm.get('title').disable(); + this.rangeChartWidgetConfigForm.get('titleFont').disable(); + this.rangeChartWidgetConfigForm.get('titleColor').disable(); + this.rangeChartWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.rangeChartWidgetConfigForm.get('iconSize').disable(); + this.rangeChartWidgetConfigForm.get('iconSizeUnit').disable(); + this.rangeChartWidgetConfigForm.get('icon').disable(); + this.rangeChartWidgetConfigForm.get('iconColor').disable(); + } + + if (showLegend) { + this.rangeChartWidgetConfigForm.get('legendPosition').enable(); + this.rangeChartWidgetConfigForm.get('legendLabelFont').enable(); + this.rangeChartWidgetConfigForm.get('legendLabelColor').enable(); + } else { + this.rangeChartWidgetConfigForm.get('legendPosition').disable(); + this.rangeChartWidgetConfigForm.get('legendLabelFont').disable(); + this.rangeChartWidgetConfigForm.get('legendLabelColor').disable(); + } + + if (showTooltip) { + this.rangeChartWidgetConfigForm.get('tooltipValueFont').enable(); + this.rangeChartWidgetConfigForm.get('tooltipValueColor').enable(); + this.rangeChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false}); + this.rangeChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); + this.rangeChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); + if (tooltipShowDate) { + this.rangeChartWidgetConfigForm.get('tooltipDateFormat').enable(); + this.rangeChartWidgetConfigForm.get('tooltipDateFont').enable(); + this.rangeChartWidgetConfigForm.get('tooltipDateColor').enable(); + } else { + this.rangeChartWidgetConfigForm.get('tooltipDateFormat').disable(); + this.rangeChartWidgetConfigForm.get('tooltipDateFont').disable(); + this.rangeChartWidgetConfigForm.get('tooltipDateColor').disable(); + } + } else { + this.rangeChartWidgetConfigForm.get('tooltipValueFont').disable(); + this.rangeChartWidgetConfigForm.get('tooltipValueColor').disable(); + this.rangeChartWidgetConfigForm.get('tooltipShowDate').disable({emitEvent: false}); + this.rangeChartWidgetConfigForm.get('tooltipDateFormat').disable(); + this.rangeChartWidgetConfigForm.get('tooltipDateFont').disable(); + this.rangeChartWidgetConfigForm.get('tooltipDateColor').disable(); + this.rangeChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); + this.rangeChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _tooltipValuePreviewFn(): string { + const units: string = this.rangeChartWidgetConfigForm.get('units').value; + const decimals: number = this.rangeChartWidgetConfigForm.get('decimals').value; + return formatValue(22, decimals, units, false); + } + + private _tooltipDatePreviewFn(): string { + const dateFormat: DateFormatSettings = this.rangeChartWidgetConfigForm.get('tooltipDateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index feeb1fe802..0cca709152 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -48,6 +48,7 @@ import { Router } from '@angular/router'; import { TbInject } from '@shared/decorators/tb-inject'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { UserSettingsService } from '@core/http/user-settings.service'; +import { ImagePipe } from '@shared/pipe/image.pipe'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -87,6 +88,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.userSettingsService = $injector.get(UserSettingsService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.date = $injector.get(DatePipe); + this.ctx.imagePipe = $injector.get(ImagePipe); this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe); this.ctx.translate = $injector.get(TranslateService); this.ctx.http = $injector.get(HttpClient); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html index 8133acedfb..e4f620b16e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts index 874d6ca1b4..863c42985a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -51,6 +51,8 @@ import { DataKey } from '@shared/models/widget.models'; import { formatNumberValue, formatValue, isDefined, isDefinedAndNotNull, isNumeric } from '@core/utils'; import { map } from 'rxjs/operators'; import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const valuesLayoutHeight = 66; const valuesLayoutVerticalPadding = 16; @@ -96,7 +98,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit dateStyle: ComponentStyle = {}; dateColor: string; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; private flot: TbFlot; @@ -109,7 +111,9 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit private panelResize$: ResizeObserver; - constructor(private renderer: Renderer2, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, private cd: ChangeDetectorRef) { } @@ -151,7 +155,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit this.dateStyle = textStyle(this.settings.dateFont); this.dateColor = this.settings.dateColor; - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html index 3f788aaf82..71b41ffc88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts index 43d2c6300c..066f0eff39 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts @@ -43,6 +43,9 @@ import { import { WidgetComponent } from '@home/components/widget/widget.component'; import { progressBarDefaultSettings, ProgressBarLayout, ProgressBarWidgetSettings } from './progress-bar-widget.models'; import { ResizeObserver } from '@juggle/resize-observer'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const defaultLayoutHeight = 80; const simplifiedLayoutHeight = 75; @@ -93,7 +96,7 @@ export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewI value: number; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; progressBarPanelResize$: ResizeObserver; @@ -102,6 +105,8 @@ export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewI private units = ''; constructor(private date: DatePipe, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, private widgetComponent: WidgetComponent, private renderer: Renderer2, private cd: ChangeDetectorRef) { @@ -144,7 +149,7 @@ export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewI this.ticksStyle.color = this.settings.ticksColor; } - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html index 16ad55078e..5ee040558a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts index a80d1d850b..48d2f5db41 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts @@ -44,6 +44,8 @@ import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } fr import { WidgetComponent } from '@home/components/widget/widget.component'; import { Observable } from 'rxjs'; import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const squareLayoutSize = 160; const squareLayoutPadding = 48; @@ -92,7 +94,7 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro dateStyle: ComponentStyle = {}; dateColor: ColorProcessor; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; private panelResize$: ResizeObserver; @@ -101,7 +103,9 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro private decimals = 0; private units = ''; - constructor(private renderer: Renderer2, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, private widgetComponent: WidgetComponent, private cd: ChangeDetectorRef) { } @@ -142,7 +146,7 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro this.dateStyle = textStyle(this.settings.dateFont); this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor); - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html index b1789800eb..b8847a5ba0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts index b187aa7969..9a1317f87e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -48,6 +48,9 @@ import { TbFlot } from '@home/components/widget/lib/flot-widget'; import { DataKey } from '@shared/models/widget.models'; import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; import { getTsValueByLatestDataKey } from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const layoutHeight = 56; const valueRelativeMaxWidth = 0.5; @@ -84,7 +87,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD valueStyle: ComponentStyle = {}; valueColor: ColorProcessor; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; private flot: TbFlot; @@ -98,7 +101,9 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD private valueFontSize: number; - constructor(private renderer: Renderer2, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, private widgetComponent: WidgetComponent, private cd: ChangeDetectorRef) { } @@ -129,7 +134,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD this.valueStyle = textStyle(this.settings.valueFont); this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html index 5c74c160f6..c39d277355 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts index 1595c59110..928445d316 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts @@ -53,6 +53,9 @@ import { DataKey } from '@shared/models/widget.models'; import { TooltipComponent, TooltipComponentOption } from 'echarts/components'; import { PieChart, PieSeriesOption } from 'echarts/charts'; import { SVGRenderer } from 'echarts/renderers'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; echarts.use([ TooltipComponent, @@ -117,7 +120,7 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit totalValueColor: ColorProcessor; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; legendItems: DoughnutLegendItem[]; @@ -145,7 +148,9 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit private svgShape: Svg; private totalTextNode: Text; - constructor(private widgetComponent: WidgetComponent, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private widgetComponent: WidgetComponent, private renderer: Renderer2, private translate: TranslateService, private cd: ChangeDetectorRef) { @@ -167,7 +172,7 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor); } - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); if (this.showLegend) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html new file mode 100644 index 0000000000..62b7784aa4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -0,0 +1,34 @@ + +
+
+ +
+
+
+
+
+
+
+
{{ rangeItem.label }}
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss new file mode 100644 index 0000000000..d9b42c6acb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.scss @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-range-chart-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-range-chart-overlay) { + z-index: 1; + } + .tb-range-chart-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-range-chart-content { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + gap: 16px; + &.legend-top { + flex-direction: column-reverse; + } + &.legend-right { + flex-direction: row; + } + &.legend-left { + flex-direction: row-reverse; + } + .tb-range-chart-shape { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + } + .tb-range-chart-legend { + display: flex; + justify-content: center; + align-items: center; + align-self: stretch; + flex-wrap: wrap; + column-gap: 24px; + row-gap: 8px; + .tb-range-chart-legend-item { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + user-select: none; + cursor: pointer; + .tb-range-chart-legend-item-label { + display: flex; + align-items: center; + gap: 4px; + color: #ccc; + .tb-range-chart-legend-item-label-circle { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ccc; + } + } + } + } + &.legend-right, &.legend-left { + gap: 24px; + .tb-range-chart-legend { + flex-direction: column-reverse; + justify-content: flex-end; + align-items: stretch; + .tb-range-chart-legend-item { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts new file mode 100644 index 0000000000..4625e4a2a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -0,0 +1,532 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { + backgroundStyle, + ColorRange, + ComponentStyle, + DateFormatProcessor, filterIncludingColorRanges, + getDataKey, + overlayStyle, + sortedColorRange, + textStyle +} from '@shared/models/widget-settings.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import * as echarts from 'echarts/core'; +import { formatValue, isDefinedAndNotNull, isNumber } from '@core/utils'; +import { + DataZoomComponent, + DataZoomComponentOption, + GridComponent, + GridComponentOption, + MarkLineComponent, + MarkLineComponentOption, + TooltipComponent, + TooltipComponentOption, + VisualMapComponent, + VisualMapComponentOption +} from 'echarts/components'; +import { LineChart, LineSeriesOption, } from 'echarts/charts'; +import { CanvasRenderer } from 'echarts/renderers'; +import { rangeChartDefaultSettings, RangeChartWidgetSettings } from './range-chart-widget.models'; +import { DataSet } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; + +echarts.use([ + TooltipComponent, + GridComponent, + VisualMapComponent, + DataZoomComponent, + MarkLineComponent, + LineChart, + CanvasRenderer +]); + +type EChartsOption = echarts.ComposeOption< + | TooltipComponentOption + | GridComponentOption + | VisualMapComponentOption + | DataZoomComponentOption + | MarkLineComponentOption + | LineSeriesOption +>; + +type ECharts = echarts.ECharts; + +interface VisualPiece { + lt?: number; + gt?: number; + lte?: number; + gte?: number; + value?: number; + color?: string; +} + +interface RangeItem { + index: number; + from?: number; + to?: number; + piece: VisualPiece; + color: string; + label: string; + visible: boolean; + enabled: boolean; +} + +const rangeItemLabel = (from?: number, to?: number): string => { + if (isNumber(from) && isNumber(to)) { + if (from === to) { + return `${from}`; + } else { + return `${from} - ${to}`; + } + } else if (isNumber(from)) { + return `≥ ${from}`; + } else if (isNumber(to)) { + return `< ${to}`; + } else { + return null; + } +}; + +const toVisualPiece = (color: string, from?: number, to?: number): VisualPiece => { + const piece: VisualPiece = { + color + }; + if (isNumber(from) && isNumber(to)) { + if (from === to) { + piece.value = from; + } else { + piece.gte = from; + piece.lt = to; + } + } else if (isNumber(from)) { + piece.gte = from; + } else if (isNumber(to)) { + piece.lt = to; + } + return piece; +}; + +const toRangeItems = (colorRanges: Array): RangeItem[] => { + const rangeItems: RangeItem[] = []; + let counter = 0; + const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to)); + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i]; + let from = range.from; + const to = range.to; + if (i > 0) { + const prevRange = ranges[i - 1]; + if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { + from = prevRange.to; + } + } + rangeItems.push( + { + index: counter++, + color: range.color, + enabled: true, + visible: true, + from, + to, + label: rangeItemLabel(from, to), + piece: toVisualPiece(range.color, from, to) + } + ); + if (!isNumber(from) || !isNumber(to)) { + const value = !isNumber(from) ? to : from; + rangeItems.push( + { + index: counter++, + color: 'transparent', + enabled: true, + visible: false, + label: '', + piece: { gt: value - 0.000000001, lt: value + 0.000000001, color: 'transparent'} + } + ); + } + } + return rangeItems; +}; + +const toNamedData = (data: DataSet): {name: string; value: [number, any]}[] => { + if (!data?.length) { + return []; + } else { + return data.map(d => ({ + name: d[0] + '', + value: d + })); + } +}; + +const getMarkPoints = (ranges: Array): number[] => { + const points = new Set(); + for (const range of ranges) { + if (range.visible) { + if (isNumber(range.from)) { + points.add(range.from); + } + if (isNumber(range.to)) { + points.add(range.to); + } + } + } + return Array.from(points).sort(); +}; + +@Component({ + selector: 'tb-range-chart-widget', + templateUrl: './range-chart-widget.component.html', + styleUrls: ['./range-chart-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('chartShape', {static: false}) + chartShape: ElementRef; + + settings: RangeChartWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + showLegend: boolean; + legendClass: string; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + legendLabelStyle: ComponentStyle; + disabledLegendLabelStyle: ComponentStyle; + visibleRangeItems: RangeItem[]; + + private rangeItems: RangeItem[]; + + private shapeResize$: ResizeObserver; + + private decimals = 0; + private units = ''; + + private drawChartPending = false; + private rangeChart: ECharts; + private rangeChartOptions: EChartsOption; + private selectedRanges: {[key: number]: boolean} = {}; + + private tooltipDateFormat: DateFormatProcessor; + + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.rangeChartWidget = this; + this.settings = {...rangeChartDefaultSettings, ...this.ctx.settings}; + + this.decimals = this.ctx.decimals; + this.units = this.ctx.units; + const dataKey = getDataKey(this.ctx.datasources); + if (isDefinedAndNotNull(dataKey?.decimals)) { + this.decimals = dataKey.decimals; + } + if (dataKey?.units) { + this.units = dataKey.units; + } + + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + this.rangeItems = toRangeItems(this.settings.rangeColors); + this.visibleRangeItems = this.rangeItems.filter(item => item.visible); + for (const range of this.rangeItems) { + this.selectedRanges[range.index] = true; + } + + this.showLegend = this.settings.showLegend && !!this.rangeItems.length; + + if (this.showLegend) { + this.legendClass = `legend-${this.settings.legendPosition}`; + this.legendLabelStyle = textStyle(this.settings.legendLabelFont); + this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); + this.legendLabelStyle.color = this.settings.legendLabelColor; + } + + if (this.settings.showTooltip && this.settings.tooltipShowDate) { + this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + } + } + + ngAfterViewInit() { + if (this.drawChartPending) { + this.drawChart(); + } + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + if (this.rangeChart) { + this.rangeChart.dispose(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + if (this.chartShape) { + this.drawChart(); + } else { + this.drawChartPending = true; + } + this.cd.detectChanges(); + } + + public onDataUpdated() { + if (this.rangeChart) { + this.rangeChart.setOption({ + xAxis: { + min: this.ctx.defaultSubscription.timeWindow.minTime, + max: this.ctx.defaultSubscription.timeWindow.maxTime + }, + series: [ + {data: this.ctx.data?.length ? toNamedData(this.ctx.data[0].data) : []} + ], + visualMap: { + selected: this.selectedRanges + } + }); + } + } + + public toggleRangeItem(item: RangeItem) { + item.enabled = !item.enabled; + this.selectedRanges[item.index] = item.enabled; + this.rangeChart.dispatchAction({ + type: 'selectDataRange', + selected: this.selectedRanges + }); + } + + private drawChart() { + const dataKey = getDataKey(this.ctx.datasources); + this.rangeChart = echarts.init(this.chartShape.nativeElement, null, { + renderer: 'canvas', + }); + this.rangeChartOptions = { + tooltip: { + trigger: 'none' + }, + grid: { + containLabel: true, + top: '30', + left: 0, + right: 0, + bottom: this.settings.dataZoom ? 60 : 0 + }, + xAxis: { + type: 'time', + axisTick: { + show: true + }, + axisLabel: { + hideOverlap: true, + fontSize: 10 + }, + axisLine: { + onZero: false + }, + min: this.ctx.defaultSubscription.timeWindow.minTime, + max: this.ctx.defaultSubscription.timeWindow.maxTime + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: value => formatValue(value, this.decimals, this.units, false) + } + }, + series: [{ + type: 'line', + name: dataKey?.label, + smooth: false, + showSymbol: false, + animation: true, + areaStyle: this.settings.fillArea ? {} : undefined, + data: this.ctx.data?.length ? toNamedData(this.ctx.data[0].data) : [], + markLine: this.rangeItems.length ? { + animation: true, + symbol: ['circle', 'arrow'], + symbolSize: [5, 7], + lineStyle: { + width: 1, + type: [3, 3], + color: '#37383b' + }, + label: { + position: 'insideEndTop', + color: '#37383b', + backgroundColor: 'rgba(255,255,255,0.56)', + padding: [4, 5], + borderRadius: 4, + formatter: params => formatValue(params.value, this.decimals, this.units, false) + }, + emphasis: { + disabled: true + }, + data: getMarkPoints(this.rangeItems).map(point => ({ yAxis: point })) + } : undefined + }], + dataZoom: [ + { + type: 'inside', + disabled: !this.settings.dataZoom + }, + { + type: 'slider', + show: this.settings.dataZoom, + showDetail: false, + right: 10 + } + ], + visualMap: { + show: false, + type: 'piecewise', + selected: this.selectedRanges, + pieces: this.rangeItems.map(item => item.piece), + outOfRange: { + color: this.settings.outOfRangeColor + }, + inRange: !this.rangeItems.length ? { + color: this.settings.outOfRangeColor + } : undefined + } + }; + + if (this.settings.showTooltip) { + this.rangeChartOptions.tooltip = { + trigger: 'axis', + formatter: (params) => { + if (!params.length || !params[0]) { + return null; + } + const seriesParams = params[0]; + const value = formatValue(seriesParams.value[1], this.decimals, this.units, false); + const tooltipElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.setStyle(tooltipElement, 'display', 'flex'); + this.renderer.setStyle(tooltipElement, 'flex-direction', 'column'); + this.renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); + this.renderer.setStyle(tooltipElement, 'gap', '4px'); + if (this.settings.tooltipShowDate) { + const dateElement: HTMLElement = this.renderer.createElement('div'); + const ts = seriesParams.value[0]; + this.tooltipDateFormat.update(ts); + this.renderer.appendChild(dateElement, this.renderer.createText(this.tooltipDateFormat.formatted)); + this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family); + this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit); + this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style); + this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight); + this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight); + this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor); + this.renderer.appendChild(tooltipElement, dateElement); + } + const labelValueElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.setStyle(labelValueElement, 'display', 'flex'); + this.renderer.setStyle(labelValueElement, 'flex-direction', 'row'); + this.renderer.setStyle(labelValueElement, 'align-items', 'center'); + this.renderer.setStyle(labelValueElement, 'align-self', 'stretch'); + this.renderer.setStyle(labelValueElement, 'gap', '12px'); + this.renderer.appendChild(tooltipElement, labelValueElement); + const labelElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.setStyle(labelElement, 'display', 'flex'); + this.renderer.setStyle(labelElement, 'align-items', 'center'); + this.renderer.setStyle(labelElement, 'gap', '8px'); + this.renderer.appendChild(labelValueElement, labelElement); + const circleElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.setStyle(circleElement, 'width', '8px'); + this.renderer.setStyle(circleElement, 'height', '8px'); + this.renderer.setStyle(circleElement, 'border-radius', '50%'); + this.renderer.setStyle(circleElement, 'background', seriesParams.color); + this.renderer.appendChild(labelElement, circleElement); + const labelTextElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.appendChild(labelTextElement, this.renderer.createText(seriesParams.seriesName)); + this.renderer.setStyle(labelTextElement, 'font-family', 'Roboto'); + this.renderer.setStyle(labelTextElement, 'font-size', '12px'); + this.renderer.setStyle(labelTextElement, 'font-style', 'normal'); + this.renderer.setStyle(labelTextElement, 'font-weight', '400'); + this.renderer.setStyle(labelTextElement, 'line-height', '16px'); + this.renderer.setStyle(labelTextElement, 'letter-spacing', '0.4px'); + this.renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)'); + this.renderer.appendChild(labelElement, labelTextElement); + const valueElement: HTMLElement = this.renderer.createElement('div'); + this.renderer.appendChild(valueElement, this.renderer.createText(value)); + this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); + this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); + this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); + this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); + this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); + this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); + this.renderer.appendChild(labelValueElement, valueElement); + return tooltipElement; + }, + padding: [8, 12], + backgroundColor: this.settings.tooltipBackgroundColor, + extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` + }; + } + + this.rangeChart.setOption(this.rangeChartOptions); + + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.chartShape.nativeElement); + this.onResize(); + } + + private onResize() { + const width = this.rangeChart.getWidth(); + const height = this.rangeChart.getHeight(); + const shapeWidth = this.chartShape.nativeElement.offsetWidth; + const shapeHeight = this.chartShape.nativeElement.offsetHeight; + if (width !== shapeWidth || height !== shapeHeight) { + this.rangeChart.resize(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts new file mode 100644 index 0000000000..6bcd0c1497 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -0,0 +1,103 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + BackgroundSettings, + BackgroundType, + ColorRange, + DateFormatSettings, + Font, simpleDateFormat +} from '@shared/models/widget-settings.models'; +import { LegendPosition } from '@shared/models/widget.models'; + +export interface RangeChartWidgetSettings { + dataZoom: boolean; + rangeColors: Array; + outOfRangeColor: string; + fillArea: boolean; + showLegend: boolean; + legendPosition: LegendPosition; + legendLabelFont: Font; + legendLabelColor: string; + showTooltip: boolean; + tooltipValueFont: Font; + tooltipValueColor: string; + tooltipShowDate: boolean; + tooltipDateFormat: DateFormatSettings; + tooltipDateFont: Font; + tooltipDateColor: string; + tooltipBackgroundColor: string; + tooltipBackgroundBlur: number; + background: BackgroundSettings; +} + +export const rangeChartDefaultSettings: RangeChartWidgetSettings = { + dataZoom: true, + rangeColors: [ + {to: -20, color: '#234CC7'}, + {from: -20, to: 0, color: '#305AD7'}, + {from: 0, to: 10, color: '#7191EF'}, + {from: 10, to: 20, color: '#FFA600'}, + {from: 20, to: 30, color: '#F36900'}, + {from: 30, to: 40, color: '#F04022'}, + {from: 40, color: '#D81838'} + ], + outOfRangeColor: '#ccc', + fillArea: true, + showLegend: true, + legendPosition: LegendPosition.top, + legendLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + legendLabelColor: 'rgba(0, 0, 0, 0.76)', + showTooltip: true, + tooltipValueFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + tooltipValueColor: 'rgba(0, 0, 0, 0.76)', + tooltipShowDate: true, + tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm'), + tooltipDateFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + tooltipDateColor: 'rgba(0, 0, 0, 0.76)', + tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', + tooltipBackgroundBlur: 4, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts index 0314120995..6980aca106 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts @@ -57,7 +57,7 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { forkJoin, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { ResizeObserver } from '@juggle/resize-observer'; // @dynamic diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html index cc2fe2cf8f..9eafcf7284 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts index 31f98aff6c..fd040b5599 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts @@ -46,6 +46,9 @@ import { BatteryLevelWidgetSettings } from '@home/components/widget/lib/indicator/battery-level-widget.models'; import { ResizeObserver } from '@juggle/resize-observer'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const verticalBatteryDimensions = { shapeAspectRatio: 64 / 113, @@ -125,7 +128,7 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView batteryShapeColor: ColorProcessor; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; batteryBoxResize$: ResizeObserver; @@ -136,6 +139,8 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView private units = ''; constructor(private date: DatePipe, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, private widgetComponent: WidgetComponent, private renderer: Renderer2, private cd: ChangeDetectorRef) { @@ -190,7 +195,7 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView this.batteryShapeColor = ColorProcessor.fromSettings(this.settings.batteryShapeColor); - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html index 4c14686b9c..430d9260b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index 847937581b..bd6743c2a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -58,6 +58,8 @@ import { ResourcesService } from '@core/services/resources.service'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { TranslateService } from '@ngx-translate/core'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-liquid-level-widget', @@ -76,7 +78,7 @@ export class LiquidLevelWidgetComponent implements OnInit { @Input() widgetTitlePanel: TemplateRef; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; hasCardClickAction = false; @@ -106,7 +108,9 @@ export class LiquidLevelWidgetComponent implements OnInit { private capacityUnits = Object.values(CapacityUnits); - constructor(private cd: ChangeDetectorRef, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private cd: ChangeDetectorRef, private resourcesService: ResourcesService, private translate: TranslateService) { } @@ -116,7 +120,7 @@ export class LiquidLevelWidgetComponent implements OnInit { this.settings = {...levelCardDefaultSettings, ...this.ctx.settings}; this.declareStyles(); - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html index a2bf74e942..f857c186d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts index 8427cf362b..f5e63d70dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.component.ts @@ -51,6 +51,9 @@ import { } from '@home/components/widget/lib/indicator/signal-strength-widget.models'; import tinycolor from 'tinycolor2'; import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const shapeWidth = 149; const shapeHeight = 113; @@ -104,7 +107,7 @@ export class SignalStrengthWidgetComponent implements OnInit, OnDestroy, AfterVi }; tooltipDateStyle: ComponentStyle = {}; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; shapeResize$: ResizeObserver; @@ -128,6 +131,8 @@ export class SignalStrengthWidgetComponent implements OnInit, OnDestroy, AfterVi private noData = false; constructor(public widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, private translate: TranslateService, private renderer: Renderer2, private cd: ChangeDetectorRef) { @@ -186,7 +191,7 @@ export class SignalStrengthWidgetComponent implements OnInit, OnDestroy, AfterVi this.tooltipDateLabelStyle = {...this.tooltipDateStyle, ...this.tooltipDateLabelStyle}; } - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; 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 934f9be6fe..476a755bc2 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 @@ -25,10 +25,11 @@ import { isUndefined, padValue } from '@core/utils'; -import { Observable, Observer, of } from 'rxjs'; +import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { FormattedData } from '@shared/models/widget.models'; import L from 'leaflet'; +import { ImagePipe } from '@shared/pipe/image.pipe'; export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); @@ -71,44 +72,59 @@ export function getDefCenterPosition(position): [number, number] { } -const imageAspectMap = {}; - -function imageLoader(imageUrl: string): Observable { - return new Observable((observer: Observer) => { - const image = document.createElement('img'); // support IE - image.style.position = 'absolute'; - image.style.left = '-99999px'; - image.style.top = '-99999px'; - image.onload = () => { - observer.next(image); - document.body.removeChild(image); - observer.complete(); - }; - image.onerror = err => { - observer.error(err); - document.body.removeChild(image); - observer.complete(); - }; - document.body.appendChild(image); - image.src = imageUrl; - }); +const imageAspectMap: {[key: string]: ImageWithAspect} = {}; + +const imageLoader = (imageUrl: string): Observable => new Observable((observer: Observer) => { + const image = document.createElement('img'); // support IE + image.style.position = 'absolute'; + image.style.left = '-99999px'; + image.style.top = '-99999px'; + image.onload = () => { + observer.next(image); + document.body.removeChild(image); + observer.complete(); + }; + image.onerror = err => { + observer.error(err); + document.body.removeChild(image); + observer.complete(); + }; + document.body.appendChild(image); + image.src = imageUrl; +}); + +const loadImageAspect = (imageUrl: string): Observable => + imageLoader(imageUrl).pipe(map(image => image.width / image.height)); + +export interface ImageWithAspect { + url: string; + aspect: number; } -export function aspectCache(imageUrl: string): Observable { +export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Observable => { if (imageUrl?.length) { const hash = hashCode(imageUrl); - let aspect = imageAspectMap[hash]; - if (aspect) { - return of(aspect); + let imageWithAspect = imageAspectMap[hash]; + if (imageWithAspect) { + return of(imageWithAspect); + } else { + return imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).pipe( + switchMap((res) => { + const url = res as string; + return loadImageAspect(url).pipe( + map((aspect) => { + imageWithAspect = {url, aspect}; + imageAspectMap[hash] = imageWithAspect; + return imageWithAspect; + }) + ); + }) + ); } - return imageLoader(imageUrl).pipe(map(image => { - aspect = image.width / image.height; - imageAspectMap[hash] = aspect; - return aspect; - })); + } else { + return of(null); } - return of(0); -} +}; export type TranslateFunc = (key: string, defaultTranslation?: string) => string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 64ef1a6303..7f611bc16a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -32,7 +32,7 @@ import { WidgetUnitedMapSettings } from './map-models'; import { Marker } from './markers'; -import { Observable, of } from 'rxjs'; +import { map, Observable, of, switchMap } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { Circle } from './circle'; @@ -63,6 +63,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import { ImagePipe } from '@shared/pipe/image.pipe'; export default abstract class LeafletMap { @@ -895,16 +896,27 @@ export default abstract class LeafletMap { const currentImage: MarkerImageInfo = this.options.useMarkerImageFunction ? safeExecute(this.options.parsedMarkerImageFunction, [data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; - const imageSize = `height: ${this.options.markerImageSize || 34}px; width: ${this.options.markerImageSize || 34}px;`; - const style = currentImage ? 'background-image: url(' + currentImage.url + '); ' + imageSize : ''; - this.options.icon = { icon: L.divIcon({ - html: `
{ + const size = this.options.useMarkerImageFunction && currentImage ? currentImage.size : this.options.markerImageSize; + const imageSize = `height: ${size || 34}px; width: ${size || 34}px;`; + const style = imageUrl ? 'background-image: url(' + imageUrl + '); ' + imageSize : ''; + return { icon: L.divIcon({ + html: `
` - }), size: [30, 30]}; + }), size: [size, size]}; + }) + ); + this.options.icon = null; } else { this.options.icon = null; + this.options.icon$ = null; } if (this.markers.get(data.entityName)) { m = this.updateMarker(data.entityName, data, markersData, this.options); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index bac18f5af6..50b975497d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -17,6 +17,7 @@ import { Datasource, FormattedData } from '@app/shared/models/widget.models'; import tinycolor from 'tinycolor2'; import { BaseIconOptions, Icon } from 'leaflet'; +import { Observable } from 'rxjs'; export const DEFAULT_MAP_PAGE_SIZE = 16384; export const DEFAULT_ZOOM_LEVEL = 8; @@ -337,6 +338,7 @@ export interface WidgetMarkersSettings extends MarkersSettings, WidgetToolipSett currentImage: MarkerImageInfo; tinyColor: tinycolor.Instance; icon: MarkerIconInfo; + icon$?: Observable; } export const defaultMarkersSettings: MarkersSettings = { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 5a4c80b12c..f5f302cee3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -17,11 +17,12 @@ import L, { LeafletMouseEvent } from 'leaflet'; import { MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, WidgetMarkersSettings, } from './map-models'; import { bindPopupActions, createTooltip } from './maps-utils'; -import { aspectCache, parseWithTranslation } from './common-maps-utils'; +import { loadImageWithAspect, parseWithTranslation } from './common-maps-utils'; import tinycolor from 'tinycolor2'; import { fillDataPattern, isDefined, isDefinedAndNotNull, processDataPattern, safeExecute } from '@core/utils'; import LeafletMap from './leaflet-map'; import { FormattedData } from '@shared/models/widget.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; export class Marker { @@ -154,6 +155,11 @@ export class Marker { if (this.settings.icon) { onMarkerIconReady(this.settings.icon); return; + } else if (this.settings.icon$) { + this.settings.icon$.subscribe((res) => { + onMarkerIconReady(res); + }); + return; } const currentImage: MarkerImageInfo = this.settings.useMarkerImageFunction ? safeExecute(this.settings.parsedMarkerImageFunction, @@ -167,16 +173,16 @@ export class Marker { } } if (currentImage && currentImage.url) { - aspectCache(currentImage.url).subscribe( - (aspect) => { - if (aspect) { + loadImageWithAspect(this.map.ctx.$injector.get(ImagePipe), currentImage.url).subscribe( + (aspectImage) => { + if (aspectImage?.aspect) { let width; let height; - if (aspect > 1) { + if (aspectImage.aspect > 1) { width = currentImage.size; - height = currentImage.size / aspect; + height = currentImage.size / aspectImage.aspect; } else { - width = currentImage.size * aspect; + width = currentImage.size * aspectImage.aspect; height = currentImage.size; } let iconAnchor = currentImage.markerOffset; @@ -188,7 +194,7 @@ export class Marker { popupAnchor = [width * this.tooltipOffset[0], height * this.tooltipOffset[1]]; } const icon = L.icon({ - iconUrl: currentImage.url, + iconUrl: aspectImage.url, iconSize: [width, height], iconAnchor, popupAnchor @@ -237,7 +243,15 @@ export class Marker { } createColorIconURI(color: tinycolor.Instance): string { - const svg = ``; + const svg = `` + + ``; return 'data:image/svg+xml;base64,' + btoa(svg); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index be6ee64753..1a05ec547b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -23,11 +23,10 @@ import { PosFunction, WidgetUnitedMapSettings } from '../map-models'; -import { Observable, ReplaySubject } from 'rxjs'; +import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { - aspectCache, - calculateNewPointCoordinate + calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; @@ -35,6 +34,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseFunction } from '@core/utils'; import { EntityDataPageLink } from '@shared/models/query/query.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; const maxZoom = 4; // ? @@ -122,39 +122,43 @@ export class ImageMap extends LeafletMap { } private imageFromUrl(url: string): Observable { - return aspectCache(url).pipe( - map( aspect => { - const mapImage: MapImage = { - imageUrl: url, - aspect, - update: false - }; - return mapImage; + return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( + switchMap( aspectImage => { + if (aspectImage) { + return of({ + imageUrl: aspectImage.url, + aspect: aspectImage.aspect, + update: false + }); + } else { + return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); + } } ), - catchError((e) => { - return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); - }) + catchError((e) => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl)) ); } private imageFromAlias(alias: Observable<[DataSet, boolean]>): Observable { return alias.pipe( - mergeMap(res => { + switchMap(res => { + const url = res[0][0][1]; const mapImage: MapImage = { - imageUrl: res[0][0][1], + imageUrl: null, aspect: null, update: res[1] }; - return aspectCache(mapImage.imageUrl).pipe( - map((aspect) => { - mapImage.aspect = aspect; - return mapImage; - } - ), - catchError((e) => { - return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); - }) + return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( + switchMap((aspectImage) => { + if (aspectImage) { + mapImage.aspect = aspectImage.aspect; + mapImage.imageUrl = aspectImage.url; + return of(mapImage); + } else { + return this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl); + } + }), + catchError((e) => this.imageFromUrl(defaultImageMapProviderSettings.mapImageUrl)) ); }) ); @@ -249,7 +253,7 @@ export class ImageMap extends LeafletMap { } } - extractPosition(data: FormattedData): {x: number, y: number} { + extractPosition(data: FormattedData): {x: number; y: number} { if (!data) { return null; } @@ -261,7 +265,7 @@ export class ImageMap extends LeafletMap { return {x: xPos, y: yPos}; } - positionToLatLng(position: {x: number, y: number}): L.LatLng { + positionToLatLng(position: {x: number; y: number}): L.LatLng { return this.pointToLatLng( position.x * this.width, position.y * this.height); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 3a817ff40e..a88c9e4e3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -36,7 +36,7 @@ type="text" (focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)"> - + @@ -56,7 +56,7 @@ max="{{key.settings.maxValue}}" (focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)"> - + @@ -82,7 +82,7 @@ (focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)" /> - +
- {{key.settings.icon}} + {{key.settings.icon}} - icon + icon diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index eb3b0c9abf..64fb98b386 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -103,8 +103,8 @@ interface MultipleInputWidgetDataKeySettings { invalidJsonErrorMessage?: string; useCustomIcon: boolean; icon: string; - customIcon: string ; - safeCustomIcon?: SafeUrl; + customIcon: string; + customIconUrl?: string; inputTypeNumber?: boolean; readOnly?: boolean; disabledOnCondition?: boolean; @@ -312,7 +312,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } if (dataKey.settings.useCustomIcon && isDefinedAndNotNull(dataKey.settings.customIcon)) { - dataKey.settings.safeCustomIcon = this.sanitizer.bypassSecurityTrustUrl(dataKey.settings.customIcon); + dataKey.settings.customIconUrl = dataKey.settings.customIcon; } if (dataKey.settings.useGetValueFunction && dataKey.settings.getValueFunctionBody.length) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html index d639e29631..d34e439e98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html @@ -16,10 +16,10 @@ -->
- - +
widgets.label-widget.labels
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html new file mode 100644 index 0000000000..155d09f53f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -0,0 +1,140 @@ + + +
+
widgets.range-chart.range-chart-card-style
+
+ + {{ 'widgets.range-chart.data-zoom' | translate }} + +
+
+
{{ 'widgets.range-chart.range-colors' | translate }}
+ + +
+
+
{{ 'widgets.range-chart.out-of-range-color' | translate }}
+ + +
+
+ + {{ 'widgets.range-chart.fill-area' | translate }} + +
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + +
+
{{ 'legend.position' | translate }}
+ + + + {{ legendPositionTranslationMap.get(pos) | translate }} + + + +
+
+
{{ 'widgets.range-chart.legend-label' | translate }}
+
+ + + + +
+
+
+
+
+
+ + + + + {{ 'widgets.range-chart.tooltip' | translate }} + + + + +
+
{{ 'widgets.range-chart.tooltip-value' | translate }}
+
+ + + + +
+
+
+ + {{ 'widgets.range-chart.tooltip-date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.range-chart.tooltip-background-color' | translate }}
+ + +
+
+
{{ 'widgets.range-chart.tooltip-background-blur' | translate }}
+ + +
px
+
+
+
+
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts new file mode 100644 index 0000000000..f5fa02065a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Injector } from '@angular/core'; +import { + legendPositions, + legendPositionTranslationMap, + WidgetSettings, + WidgetSettingsComponent +} from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { formatValue } from '@core/utils'; +import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/range-chart-widget.models'; +import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-range-chart-widget-settings', + templateUrl: './range-chart-widget-settings.component.html', + styleUrls: [] +}) +export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { + + legendPositions = legendPositions; + + legendPositionTranslationMap = legendPositionTranslationMap; + + rangeChartWidgetSettingsForm: UntypedFormGroup; + + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); + + tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.rangeChartWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...rangeChartDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.rangeChartWidgetSettingsForm = this.fb.group({ + dataZoom: [settings.dataZoom, []], + rangeColors: [settings.rangeColors, []], + outOfRangeColor: [settings.outOfRangeColor, []], + fillArea: [settings.fillArea, []], + + showLegend: [settings.showLegend, []], + legendPosition: [settings.legendPosition, []], + legendLabelFont: [settings.legendLabelFont, []], + legendLabelColor: [settings.legendLabelColor, []], + + showTooltip: [settings.showTooltip, []], + tooltipValueFont: [settings.tooltipValueFont, []], + tooltipValueColor: [settings.tooltipValueColor, []], + tooltipShowDate: [settings.tooltipShowDate, []], + tooltipDateFormat: [settings.tooltipDateFormat, []], + tooltipDateFont: [settings.tooltipDateFont, []], + tooltipDateColor: [settings.tooltipDateColor, []], + tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], + tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showLegend', 'showTooltip', 'tooltipShowDate']; + } + + protected updateValidators(emitEvent: boolean) { + const showLegend: boolean = this.rangeChartWidgetSettingsForm.get('showLegend').value; + const showTooltip: boolean = this.rangeChartWidgetSettingsForm.get('showTooltip').value; + const tooltipShowDate: boolean = this.rangeChartWidgetSettingsForm.get('tooltipShowDate').value; + + if (showLegend) { + this.rangeChartWidgetSettingsForm.get('legendPosition').enable(); + this.rangeChartWidgetSettingsForm.get('legendLabelFont').enable(); + this.rangeChartWidgetSettingsForm.get('legendLabelColor').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('legendPosition').disable(); + this.rangeChartWidgetSettingsForm.get('legendLabelFont').disable(); + this.rangeChartWidgetSettingsForm.get('legendLabelColor').disable(); + } + + if (showTooltip) { + this.rangeChartWidgetSettingsForm.get('tooltipValueFont').enable(); + this.rangeChartWidgetSettingsForm.get('tooltipValueColor').enable(); + this.rangeChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); + this.rangeChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); + this.rangeChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); + if (tooltipShowDate) { + this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').enable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateFont').enable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateColor').enable(); + } else { + this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateFont').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateColor').disable(); + } + } else { + this.rangeChartWidgetSettingsForm.get('tooltipValueFont').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipValueColor').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false}); + this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateFont').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipDateColor').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipBackgroundColor').disable(); + this.rangeChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); + } + } + + private _tooltipValuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, false); + } + + private _tooltipDatePreviewFn(): string { + const dateFormat: DateFormatSettings = this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html index 42c4472cca..cd406cf01e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html @@ -27,13 +27,7 @@
- -
-
widgets.background.image-url
- - - -
+
widgets.color.color
widgets.background.preview
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts index ab94b95800..6ee4ffaf00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { backgroundStyle, @@ -27,6 +27,9 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-background-settings-panel', @@ -54,11 +57,14 @@ export class BackgroundSettingsPanelComponent extends PageComponent implements O backgroundSettingsFormGroup: UntypedFormGroup; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; constructor(private fb: UntypedFormBuilder, - protected store: Store) { + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + protected store: Store, + private cd: ChangeDetectorRef) { super(store); } @@ -66,7 +72,6 @@ export class BackgroundSettingsPanelComponent extends PageComponent implements O this.backgroundSettingsFormGroup = this.fb.group( { type: [this.backgroundSettings?.type, []], - imageBase64: [this.backgroundSettings?.imageBase64, []], imageUrl: [this.backgroundSettings?.imageUrl, []], color: [this.backgroundSettings?.color, []], overlay: this.fb.group({ @@ -101,11 +106,11 @@ export class BackgroundSettingsPanelComponent extends PageComponent implements O private updateValidators() { const overlayEnabled: boolean = this.backgroundSettingsFormGroup.get('overlay').get('enabled').value; if (overlayEnabled) { - this.backgroundSettingsFormGroup.get('overlay').get('color').enable(); - this.backgroundSettingsFormGroup.get('overlay').get('blur').enable(); + this.backgroundSettingsFormGroup.get('overlay').get('color').enable({emitEvent: false}); + this.backgroundSettingsFormGroup.get('overlay').get('blur').enable({emitEvent: false}); } else { - this.backgroundSettingsFormGroup.get('overlay').get('color').disable(); - this.backgroundSettingsFormGroup.get('overlay').get('blur').disable(); + this.backgroundSettingsFormGroup.get('overlay').get('color').disable({emitEvent: false}); + this.backgroundSettingsFormGroup.get('overlay').get('blur').disable({emitEvent: false}); } this.backgroundSettingsFormGroup.get('overlay').get('color').updateValueAndValidity({emitEvent: false}); this.backgroundSettingsFormGroup.get('overlay').get('blur').updateValueAndValidity({emitEvent: false}); @@ -113,8 +118,9 @@ export class BackgroundSettingsPanelComponent extends PageComponent implements O private updateBackgroundStyle() { const background: BackgroundSettings = this.backgroundSettingsFormGroup.value; - this.backgroundStyle = backgroundStyle(background); + this.backgroundStyle$ = backgroundStyle(background, this.imagePipe, this.sanitizer, true); this.overlayStyle = overlayStyle(background.overlay); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html index e9e1b99b0e..f3304eeca0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html @@ -22,7 +22,7 @@ #matButton (click)="openBackgroundSettingsPopup($event, matButton)">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts index 62a2495942..5f49c9993c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts @@ -14,20 +14,32 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnInit, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { BackgroundSettings, backgroundStyle, BackgroundType, ComponentStyle, - overlayStyle + overlayStyle, validateAndUpdateBackgroundSettings } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { BackgroundSettingsPanelComponent } from '@home/components/widget/lib/settings/common/background-settings-panel.component'; +import { Observable, of } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-background-settings', @@ -45,21 +57,24 @@ import { export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor { @Input() - disabled: boolean; + disabled = false; backgroundType = BackgroundType; modelValue: BackgroundSettings; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; private propagateChange = null; - constructor(private popoverService: TbPopoverService, + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private popoverService: TbPopoverService, private renderer: Renderer2, - private viewContainerRef: ViewContainerRef) {} + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) {} ngOnInit(): void { } @@ -72,12 +87,14 @@ export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor } setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - this.updateBackgroundStyle(); + if (this.disabled !== isDisabled) { + this.disabled = isDisabled; + this.updateBackgroundStyle(); + } } writeValue(value: BackgroundSettings): void { - this.modelValue = value; + this.modelValue = validateAndUpdateBackgroundSettings(value); this.updateBackgroundStyle(); } @@ -109,12 +126,13 @@ export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor private updateBackgroundStyle() { if (!this.disabled) { - this.backgroundStyle = backgroundStyle(this.modelValue); + this.backgroundStyle$ = backgroundStyle(this.modelValue, this.imagePipe, this.sanitizer, true); this.overlayStyle = overlayStyle(this.modelValue.overlay); } else { - this.backgroundStyle = {}; + this.backgroundStyle$ = of({}); this.overlayStyle = {}; } + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts index 989bdb2844..31cafc6d96 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-settings.component.ts @@ -147,7 +147,9 @@ export class ColorRangeSettingsComponent implements OnInit, ControlValueAccessor const rangeColors = this.modelValue.slice(0, Math.min(3, this.modelValue.length)).map(r => r.color); colors = colors.concat(rangeColors); } - if (colors.length === 1) { + if (!colors.length) { + this.colorStyle = {}; + } else if (colors.length === 1) { this.colorStyle = {backgroundColor: colors[0]}; } else { const gradientValues: string[] = []; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts index e4f0b47946..2aba8866ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts @@ -16,11 +16,7 @@ import { Component, forwardRef, Input, OnInit, Renderer2, ViewChild, ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; -import { - compareDateFormats, - dateFormats, - DateFormatSettings -} from '@shared/models/widget-settings.models'; +import { compareDateFormats, dateFormats, DateFormatSettings } from '@shared/models/widget-settings.models'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatButton } from '@angular/material/button'; @@ -29,6 +25,7 @@ import { deepClone } from '@core/utils'; import { DateFormatSettingsPanelComponent } from '@home/components/widget/lib/settings/common/date-format-settings-panel.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-date-format-select', @@ -50,7 +47,11 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { @Input() disabled: boolean; - dateFormatList = dateFormats; + @Input() + @coerceBoolean() + excludeLastUpdateAgo = false; + + dateFormatList: DateFormatSettings[]; dateFormatsCompare = compareDateFormats; @@ -69,6 +70,8 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor { private viewContainerRef: ViewContainerRef) {} ngOnInit(): void { + this.dateFormatList = this.excludeLastUpdateAgo ? + dateFormats.filter(format => !format.lastUpdateAgo) : dateFormats; this.dateFormatFormControl = new UntypedFormControl(); this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => { this.updateModel(value); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index 22571baab4..f3c745a665 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -245,10 +245,10 @@ iconClearButton formControlName="icon"> - - +
widgets.input-widgets.value-conversion-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html index c111f84e9f..a9d7a2c8ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html @@ -16,9 +16,9 @@ -->
- - +
widgets.maps.image-map-background-from-entity-attribute
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html index 2b5de971b5..8de4820690 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html @@ -170,10 +170,10 @@ - - + widgets.maps.custom-marker-image-size @@ -186,10 +186,10 @@ functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}" helpId="widget/lib/map/marker_image_fn"> - - + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html index fd14bebdf7..0f31ab54a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html @@ -70,10 +70,10 @@ - - + widgets.maps.custom-marker-image-size @@ -86,10 +86,10 @@ functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}" helpId="widget/lib/map/marker_image_fn"> - - + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 0f545366c2..5d1bce1745 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -305,6 +305,9 @@ import { import { DoughnutWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/doughnut-widget-settings.component'; +import { + RangeChartWidgetSettingsComponent +} from '@home/components/widget/lib/settings/chart/range-chart-widget-settings.component'; @NgModule({ declarations: [ @@ -416,7 +419,8 @@ import { ValueChartCardWidgetSettingsComponent, ProgressBarWidgetSettingsComponent, LiquidLevelCardWidgetSettingsComponent, - DoughnutWidgetSettingsComponent + DoughnutWidgetSettingsComponent, + RangeChartWidgetSettingsComponent ], imports: [ CommonModule, @@ -533,7 +537,8 @@ import { ValueChartCardWidgetSettingsComponent, ProgressBarWidgetSettingsComponent, LiquidLevelCardWidgetSettingsComponent, - DoughnutWidgetSettingsComponent + DoughnutWidgetSettingsComponent, + RangeChartWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -616,5 +621,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts index bf43218f82..be836dba17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts @@ -47,6 +47,9 @@ import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { Path, Svg, SVG, Text } from '@svgdotjs/svg.js'; import { DataKey } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const shapeSize = 180; const cx = shapeSize / 2; @@ -87,7 +90,7 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft centerValueColor: ColorProcessor; - backgroundStyle: ComponentStyle = {}; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; shapeResize$: ResizeObserver; @@ -109,6 +112,8 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft private centerValueText = 'N/A'; constructor(private widgetComponent: WidgetComponent, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, private renderer: Renderer2, private cd: ChangeDetectorRef) { } @@ -135,7 +140,7 @@ export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, Aft this.centerValueColor = ColorProcessor.fromSettings(this.settings.centerValueColor); - this.backgroundStyle = backgroundStyle(this.settings.background); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; 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 858856fb4a..6679ed0cca 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 @@ -66,6 +66,7 @@ import { ValueChartCardWidgetComponent } from '@home/components/widget/lib/cards import { ProgressBarWidgetComponent } from '@home/components/widget/lib/cards/progress-bar-widget.component'; import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicator/liquid-level-widget.component'; import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/doughnut-widget.component'; +import { RangeChartWidgetComponent } from '@home/components/widget/lib/chart/range-chart-widget.component'; @NgModule({ declarations: @@ -106,7 +107,8 @@ import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/dough ValueChartCardWidgetComponent, ProgressBarWidgetComponent, LiquidLevelWidgetComponent, - DoughnutWidgetComponent + DoughnutWidgetComponent, + RangeChartWidgetComponent ], imports: [ CommonModule, @@ -151,7 +153,8 @@ import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/dough ValueChartCardWidgetComponent, ProgressBarWidgetComponent, LiquidLevelWidgetComponent, - DoughnutWidgetComponent + DoughnutWidgetComponent, + RangeChartWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts index 1b6af3bf43..b009000548 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -21,7 +21,7 @@ import { Observable } from 'rxjs'; import { ImportDialogCsvComponent, ImportDialogCsvData -} from '@home/components/import-export/import-dialog-csv.component'; +} from '@shared/import-export/import-dialog-csv.component'; @Injectable() export class HomeDialogsService { diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index 0608507295..4c5a8f4c04 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -33,7 +33,7 @@ import { UserService } from '@core/http/user.service'; import { AlarmService } from '@core/http/alarm.service'; import { Router } from '@angular/router'; import { BroadcastService } from '@core/services/broadcast.service'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { AssetProfileService } from '@core/http/asset-profile.service'; import { OtaPackageService } from '@core/http/ota-package.service'; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 61b4713cc4..17154f001b 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -91,7 +91,7 @@ import * as RxJSOperators from 'rxjs/operators'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models'; -import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; +import { ImagePipe, MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; @@ -189,6 +189,7 @@ export class WidgetContext { telemetryWsService: TelemetryWebsocketService; telemetrySubscribers?: TelemetrySubscriber[]; date: DatePipe; + imagePipe: ImagePipe; milliSecondsToTimeString: MillisecondsToTimeStringPipe; translate: TranslateService; http: HttpClient; diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 5d2727533c..0526313da4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -39,6 +39,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.module'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; +import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -71,6 +72,25 @@ const routes: Routes = [ } }, ...widgetsLibraryRoutes, + { + path: 'images', + data: { + breadcrumb: { + label: 'image.gallery', + icon: 'filter' + } + }, + children: [ + { + path: '', + component: ImageGalleryComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'image.gallery', + }, + } + ] + }, { path: 'resources-library', data: { diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 733e5ef2b3..52ded64e29 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -84,7 +84,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve this.translate.instant('resource.delete-resources-text'); this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); - this.config.loadEntity = id => this.resourceService.getResource(id.id); + this.config.loadEntity = id => this.resourceService.getResourceInfo(id.id); this.config.saveEntity = resource => this.saveResource(resource); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts index f33eccf090..1862a9d5c0 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts @@ -29,7 +29,7 @@ import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared import { EntityAction } from '@home/models/entity/entity-component.models'; import { DialogService } from '@core/services/dialog.service'; import { MatDialog } from '@angular/material/dialog'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; import { AssetProfile, TB_SERVICE_QUEUE } from '@shared/models/asset.models'; import { AssetProfileService } from '@core/http/asset-profile.service'; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 15485fbcb0..b1bb37470a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -139,11 +139,10 @@ dashboard.mobile-order - - + +
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 9238ac4244..6a0b987e5b 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 @@ -64,13 +64,19 @@ import { MakeDashboardPublicDialogData } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { EdgeService } from '@core/http/edge.service'; import { AddEntitiesToEdgeDialogComponent, AddEntitiesToEdgeDialogData } from '@home/dialogs/add-entities-to-edge-dialog.component'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { Widget } from '@shared/models/widget.models'; +import { EntityAliases } from '@shared/models/alias.models'; +import { + EntityAliasesDialogComponent, + EntityAliasesDialogData +} from '@home/components/alias/entity-aliases-dialog.component'; @Injectable() export class DashboardsTableConfigResolver implements Resolve> { @@ -383,7 +389,7 @@ export class DashboardsTableConfigResolver implements Resolve { if (dashboard) { this.config.updateData(); @@ -392,6 +398,30 @@ export class DashboardsTableConfigResolver implements Resolve, isSingleWidget: boolean, + customTitle: string, missingEntityAliases: EntityAliases): Observable { + return this.dialog.open(EntityAliasesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityAliases: missingEntityAliases, + widgets, + customTitle, + isSingleWidget, + disableAdd: true + } + }).afterClosed().pipe( + map((updatedEntityAliases) => { + if (updatedEntityAliases) { + return updatedEntityAliases; + } else { + throw new Error('Unable to resolve missing entity aliases!'); + } + } + )); + } + exportDashboard($event: Event, dashboard: DashboardInfo) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts index 9f026d626e..28ceb285ad 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -41,7 +41,7 @@ import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from '@home/components/profile/add-device-profile-dialog.component'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; @Injectable() 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 37714a992c..bcbf1b8844 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 @@ -36,7 +36,7 @@ import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainComponent } from '@modules/home/pages/rulechain/rulechain.component'; import { DialogService } from '@core/services/dialog.service'; import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { ItemBufferService } from '@core/services/item-buffer.service'; import { EdgeService } from '@core/http/edge.service'; import { forkJoin, Observable } from 'rxjs'; diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts index 8963e0f8da..6853e8c71c 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts @@ -28,7 +28,7 @@ import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; import { mergeMap, tap } from 'rxjs/operators'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { deepClone } from '@core/utils'; import printTemplate from '!raw-loader!./backup-code-print-template.raw'; diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts index 5701bff6bf..402a04aa72 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts @@ -29,12 +29,10 @@ import { DatePipe } from '@angular/common'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { TenantProfileService } from '@core/http/tenant-profile.service'; -import { TenantProfileComponent } from '../../components/profile/tenant-profile.component'; +import { TenantProfileComponent } from '@home/components/profile/tenant-profile.component'; import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; import { DialogService } from '@core/services/dialog.service'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { map } from 'rxjs/operators'; -import { guid } from '@core/utils'; +import { ImportExportService } from '@shared/import-export/import-export.service'; @Injectable() export class TenantProfilesTableConfigResolver implements Resolve> { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index f3410f9893..2251bd4022 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -240,11 +240,10 @@
- - + widget.description diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts index 0363aa6117..8086e7f646 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts @@ -31,16 +31,15 @@ import { WidgetService } from '@app/core/http/widget.service'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { DialogService } from '@core/services/dialog.service'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { Direction } from '@shared/models/page/sort-order'; import { BaseWidgetType, + widgetType as WidgetDataType, WidgetTypeDetails, WidgetTypeInfo, - widgetType as WidgetDataType, widgetTypesData } from '@shared/models/widget.models'; import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; @@ -134,7 +133,7 @@ export class WidgetTypesTableConfigResolver implements Resolve this.widgetsService.getWidgetTypeById(id.id); this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails); this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id); - this.config.onEntityAction = action => this.onWidgetTypeAction(action); + this.config.onEntityAction = action => this.onWidgetTypeAction(action, this.config); this.config.handleRowClick = ($event, widgetType) => { if (this.config.isDetailsOpen()) { @@ -198,6 +197,14 @@ export class WidgetTypesTableConfigResolver implements Resolve) { + if ($event) { + $event.stopPropagation(); + } + const url = this.router.createUrlTree(['details', widgetType.id.id], {relativeTo: config.getActivatedRoute()}); + this.router.navigateByUrl(url); + } + exportWidgetType($event: Event, widgetType: BaseWidgetType) { if ($event) { $event.stopPropagation(); @@ -216,11 +223,14 @@ export class WidgetTypesTableConfigResolver implements Resolve): boolean { + onWidgetTypeAction(action: EntityAction, config: EntityTableConfig): boolean { switch (action.action) { case 'edit': this.openWidgetEditor(action.event, action.entity); return true; + case 'openDetails': + this.openWidgetTypeDetails(action.event, action.entity, config); + return true; case 'export': this.exportWidgetType(action.event, action.entity); return true; diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html index fdd49c4a61..f9f4990d21 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html @@ -62,7 +62,7 @@
- {{ widget.name }} + {{ widget.name }}
{{ widget.name }}
{{ 'widget.deprecated' | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss index 3c971105ff..11c611c739 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss @@ -154,7 +154,8 @@ gap: 12px; .tb-bundle-widget-image-preview { width: 64px; - max-height: 100%; + max-height: 52px; + height: 100%; object-fit: contain; border-radius: 6px; } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts index cc54c53901..105c18c123 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts @@ -26,10 +26,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { widgetType as WidgetDataType, WidgetTypeInfo } from '@shared/models/widget.models'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; import { WidgetService } from '@core/http/widget.service'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { isDefinedAndNotNull } from '@core/utils'; import { FormControl, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; @@ -61,7 +59,6 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn private route: ActivatedRoute, private widgetsService: WidgetService, private importExport: ImportExportService, - private sanitizer: DomSanitizer, private cd: ChangeDetectorRef, private dialog: MatDialog) { super(store); @@ -86,13 +83,6 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn ngOnInit(): void { } - getPreviewImage(imageUrl: string | null): SafeUrl | string { - if (isDefinedAndNotNull(imageUrl)) { - return this.sanitizer.bypassSecurityTrustUrl(imageUrl); - } - return '/assets/widget-preview-empty.svg'; - } - trackByWidget(index: number, widget: WidgetTypeBundle): any { return widget; } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html index fcebcc1e6c..978a79ad63 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html @@ -22,6 +22,12 @@ [fxShow]="!isEdit"> {{'widgets-bundle.open-widgets-bundle' | translate }} +
-
dashboard.maximum-upload-file-size
+
+
{{ fileName }}
+
dashboard.maximum-upload-file-size
+
diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss index ee4f6451c4..36f4c59557 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.scss +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -23,9 +23,12 @@ $previewSize: 78px !default; .tb-container { margin-top: 0; padding: 0 0 16px; + display: flex; + flex-direction: column; + gap: 8px; label.tb-title { display: block; - padding-bottom: 8px; + padding-bottom: 0px; } } @@ -72,6 +75,30 @@ $previewSize: 78px !default; max-height: $previewSize - 2px; } + .tb-image-info-container { + display: flex; + gap: 12px; + font-size: 13px; + font-style: normal; + line-height: 16px; + letter-spacing: normal; + } + + .tb-image-file-name { + width: 136px; + color: rgba(0, 0, 0, 0.54); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + } + + .tb-image-file-hint { + color: rgba(0, 0, 0, 0.38); + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + .file-input { display: none; } diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts index 026b64a413..21722159d4 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, EventEmitter, + forwardRef, + Input, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,6 +36,8 @@ import { UtilsService } from '@core/services/utils.service'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ImagePipe } from '@shared/pipe/image.pipe'; @Component({ selector: 'tb-image-input', @@ -74,7 +85,27 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @Input() inputId = this.utils.guid(); + @Input() + @coerceBoolean() + processImageApiLink = false; + + @Input() + @coerceBoolean() + resultAsFile = false; + + @Input() + @coerceBoolean() + showFileName = false; + + @Input() + fileName: string; + + @Output() + fileNameChanged = new EventEmitter(); + imageUrl: string; + file: File; + safeImageUrl: SafeUrl; @ViewChild('flow', {static: true}) @@ -87,6 +118,7 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, constructor(protected store: Store, private utils: UtilsService, private sanitizer: DomSanitizer, + private imagePipe: ImagePipe, private dialog: DialogService, private translate: TranslateService, private fileSize: FileSizePipe, @@ -97,7 +129,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, ngAfterViewInit() { this.autoUploadSubscription = this.flow.events$.subscribe(event => { if (event.type === 'fileAdded') { - const file = (event.event[0] as flowjs.FlowFile).file; + const flowFile = event.event[0] as flowjs.FlowFile; + const file = flowFile.file; + const fileName = flowFile.name; if (this.maxSizeByte && this.maxSizeByte < file.size) { this.dialog.alert( this.translate.instant('dashboard.cannot-upload-file'), @@ -108,10 +142,12 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, return false; } const reader = new FileReader(); - reader.onload = (loadEvent) => { + reader.onload = (_loadEvent) => { if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { this.imageUrl = reader.result; this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + this.file = file; + this.fileName = fileName; this.updateModel(); } }; @@ -138,7 +174,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, writeValue(value: string): void { this.imageUrl = value; if (this.imageUrl) { - this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + if (this.processImageApiLink) { + this.imagePipe.transform(this.imageUrl, {preview: true, ignoreLoadingImage: true}).subscribe( + (res) => { + this.safeImageUrl = res; + } + ); + } else { + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); + } } else { this.safeImageUrl = null; } @@ -146,12 +190,19 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, private updateModel() { this.cd.markForCheck(); - this.propagateChange(this.imageUrl); + if (this.resultAsFile) { + this.propagateChange(this.file); + } else { + this.propagateChange(this.imageUrl); + } + this.fileNameChanged.emit(this.fileName); } clearImage() { this.imageUrl = null; this.safeImageUrl = null; + this.file = null; + this.fileName = null; this.updateModel(); } } diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html new file mode 100644 index 0000000000..0ef4aa4bb0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.html @@ -0,0 +1,95 @@ + +
+ +
+
+ +
+
+
+
+
+ +
+ {{ imageResource.title }} +
+
+
{{ imageResource.descriptor.width }}x{{ imageResource.descriptor.height }}
+ +
{{ imageResource.descriptor.size | fileSize }}
+
+
+
+
+ + +
+ +
+
+ + +
+
+
+ + +
{{ (disabled ? 'image.no-image' : 'image.no-image-selected') | translate }}
+
+ + + diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.scss b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.scss new file mode 100644 index 0000000000..47ae1e96a9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.scss @@ -0,0 +1,191 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../scss/constants"; + +$containerHeight: 96px !default; + +:host { + .tb-container { + margin-top: 0; + padding: 0 0 16px; + display: flex; + flex-direction: column; + gap: 8px; + label.tb-title { + display: block; + padding-bottom: 0; + } + } + + .tb-image-select-container { + width: 100%; + height: $containerHeight; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + align-items: center; + &.disabled { + width: $containerHeight; + .tb-image-container { + width: $containerHeight - 2px; + border-right: none; + } + } + } + + .tb-image-container { + width: $containerHeight - 1px; + height: $containerHeight - 2px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + border-right: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + overflow: hidden; + } + + .tb-image-preview { + width: auto; + max-width: $containerHeight - 2px; + height: auto; + max-height: $containerHeight - 2px; + } + + .tb-no-image { + text-align: center; + color: rgba(0, 0, 0, 0.38); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + + .tb-image-info-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 0 8px; + justify-content: flex-end; + align-items: center; + gap: 4px; + .tb-base64-image-container, .tb-resource-image-container, .tb-external-image-container { + display: flex; + flex: 1; + align-self: stretch; + } + .tb-resource-image-container { + padding: 8px; + justify-content: center; + align-items: flex-start; + flex-direction: column; + gap: 4px; + .tb-resource-image-name { + color: rgba(0, 0, 0, 0.54); + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.25px; + } + .tb-resource-image-details { + display: flex; + align-items: center; + gap: 8px; + color: rgba(0, 0, 0, 0.38); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.017px; + .mat-divider.mat-divider-vertical { + height: 16px; + } + } + &.loading { + align-items: center; + } + } + + .tb-external-image-container { + padding: 16px 8px 0 16px; + flex-direction: column; + align-items: flex-start; + gap: 4px; + .tb-external-link-label { + color: rgba(0, 0, 0, 0.54); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + .tb-external-link-input-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 4px; + align-self: stretch; + .tb-inline-field { + width: 100%; + } + } + } + + .tb-image-clear-btn { + color: rgba(0,0,0,0.38); + } + } + + .tb-image-select-buttons-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 8px; + gap: 8px; + justify-content: center; + align-items: flex-start; + .tb-image-select-button { + width: 100%; + height: 100%; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + padding: 8px; + line-height: normal; + font-size: 12px; + @media #{$mat-gt-xs} { + padding: 16px; + } + .mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + margin: 0; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts new file mode 100644 index 0000000000..2a82ff1dc0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts @@ -0,0 +1,225 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnDestroy, + OnInit, + Renderer2, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + extractParamsFromImageResourceUrl, + ImageResourceInfo, + isBase64DataImageUrl, + isImageResourceUrl, prependTbImagePrefix, removeTbImagePrefix +} from '@shared/models/resource.models'; +import { ImageService } from '@core/http/image.service'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; + +export enum ImageLinkType { + none = 'none', + base64 = 'base64', + external = 'external', + resource = 'resource' +} + +@Component({ + selector: 'tb-gallery-image-input', + templateUrl: './gallery-image-input.component.html', + styleUrls: ['./gallery-image-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GalleryImageInputComponent), + multi: true + } + ] +}) +export class GalleryImageInputComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @Input() + label: string; + + @Input() + @coerceBoolean() + required = false; + + @Input() + disabled: boolean; + + imageUrl: string; + + imageResource: ImageResourceInfo; + + loadingImageResource = false; + + ImageLinkType = ImageLinkType; + + linkType: ImageLinkType = ImageLinkType.none; + + externalLinkControl = new FormControl(null); + + private propagateChange = null; + + constructor(protected store: Store, + private imageService: ImageService, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private popoverService: TbPopoverService) { + super(store); + } + + ngOnInit() { + this.externalLinkControl.valueChanges.subscribe((value) => { + if (this.linkType === ImageLinkType.external) { + this.updateModel(value); + } + }); + } + + ngOnDestroy() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.detectLinkType(); + this.externalLinkControl.disable({emitEvent: false}); + } else { + this.externalLinkControl.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + value = removeTbImagePrefix(value); + if (this.imageUrl !== value) { + this.reset(); + this.imageUrl = value; + this.detectLinkType(); + if (this.linkType === ImageLinkType.resource) { + const params = extractParamsFromImageResourceUrl(this.imageUrl); + this.loadingImageResource = true; + this.imageService.getImageInfo(params.type, params.key, {ignoreLoading: true, ignoreErrors: true}).subscribe( + { + next: (res) => { + this.imageResource = res; + this.loadingImageResource = false; + this.cd.markForCheck(); + }, + error: () => { + this.reset(); + this.loadingImageResource = false; + this.cd.markForCheck(); + } + } + ); + } else if (this.linkType === ImageLinkType.base64) { + this.cd.markForCheck(); + } else if (this.linkType === ImageLinkType.external) { + this.externalLinkControl.setValue(this.imageUrl, {emitEvent: false}); + this.cd.markForCheck(); + } + } + } + + private detectLinkType() { + if (this.imageUrl) { + if (isImageResourceUrl(this.imageUrl)) { + this.linkType = ImageLinkType.resource; + } else if (isBase64DataImageUrl(this.imageUrl)) { + this.linkType = ImageLinkType.base64; + } else { + this.linkType = ImageLinkType.external; + } + } else { + this.linkType = ImageLinkType.none; + } + } + + private updateModel(value: string) { + this.cd.markForCheck(); + if (this.imageUrl !== value) { + this.imageUrl = value; + this.propagateChange(prependTbImagePrefix(this.imageUrl)); + } + } + + private reset() { + this.linkType = ImageLinkType.none; + this.imageResource = null; + this.externalLinkControl.setValue(null, {emitEvent: false}); + } + + clearImage() { + this.reset(); + this.updateModel(null); + } + + setLink($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.linkType = ImageLinkType.external; + } + + toggleGallery($event: Event, browseGalleryButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = browseGalleryButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + pageMode: false, + popoverMode: true, + mode: 'grid', + selectionMode: true + }; + const imageGalleryPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ImageGalleryComponent, 'top', true, null, + ctx, + {}, + {}, {}, true); + imageGalleryPopover.tbComponentRef.instance.imageSelected.subscribe((image) => { + imageGalleryPopover.hide(); + this.linkType = ImageLinkType.resource; + this.imageResource = image; + this.updateModel(image.link); + }); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html new file mode 100644 index 0000000000..05bc4e943c --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -0,0 +1,112 @@ + +
+ +

{{ (readonly ? 'image.image-details' : 'image.edit-image') | translate }}

+ + +
+ + +
+
+
+ + image.name + + + {{ 'image.name-required' | translate }} + + + + + + image.link + + + + +
+
+
+
+ + +
+ +
+
+
+ +
+
+
{{ image.descriptor.width }}x{{ image.descriptor.height }}
+ +
{{ image.descriptor.size | fileSize }}
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.scss b/ui-ngx/src/app/shared/components/image/image-dialog.component.scss new file mode 100644 index 0000000000..497714b6dd --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.scss @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../scss/constants'; + +:host { + .tb-image-dialog { + @media #{$mat-gt-xs} { + width: 50vh; + } + .mat-mdc-dialog-content { + max-height: 100%; + } + fieldset { + height: 100%; + display: flex; + flex-direction: column; + } + } + .tb-image-container { + height: 100%; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.05); + padding: 12px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .tb-image-content { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + .tb-image-preview-container { + position: relative; + width: 100%; + height: 100%; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + .tb-image-preview-spacer { + @media #{$mat-gt-xs} { + margin-top: 100%; + } + } + .tb-image-preview { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + } + } + .tb-image-preview-details { + display: flex; + align-items: center; + gap: 8px; + color: rgba(0, 0, 0, 0.38); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + .mat-divider.mat-divider-vertical { + height: 20px; + } + } + .tb-image-actions { + display: flex; + align-items: center; + align-self: stretch; + justify-content: space-between; + gap: 8px; + color: rgba(0,0,0,0.54); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.ts b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts new file mode 100644 index 0000000000..6904a30096 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { ImageService } from '@core/http/image.service'; +import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models'; +import { + UploadImageDialogComponent, + UploadImageDialogData +} from '@shared/components/image/upload-image-dialog.component'; +import { UrlHolder } from '@shared/pipe/image.pipe'; +import { ImportExportService } from '@shared/import-export/import-export.service'; + +export interface ImageDialogData { + readonly: boolean; + image: ImageResourceInfo; +} + +@Component({ + selector: 'tb-image-dialog', + templateUrl: './image-dialog.component.html', + styleUrls: ['./image-dialog.component.scss'] +}) +export class ImageDialogComponent extends + DialogComponent implements OnInit { + + image: ImageResourceInfo; + + readonly: boolean; + + imageFormGroup: UntypedFormGroup; + + imageChanged = false; + + imagePreviewData: UrlHolder; + + constructor(protected store: Store, + protected router: Router, + private imageService: ImageService, + private dialog: MatDialog, + private importExportService: ImportExportService, + @Inject(MAT_DIALOG_DATA) private data: ImageDialogData, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + this.image = data.image; + this.readonly = data.readonly; + this.imagePreviewData = { + url: this.image.link + }; + } + + ngOnInit(): void { + this.imageFormGroup = this.fb.group({ + title: [this.image.title, [Validators.required]], + link: [this.image.link, []], + }); + if (this.data.readonly) { + this.imageFormGroup.disable(); + } else { + this.imageFormGroup.get('link').disable(); + } + } + + cancel(): void { + this.dialogRef.close(this.imageChanged ? this.image : null); + } + + revertInfo(): void { + this.imageFormGroup.get('title').setValue(this.image.title); + this.imageFormGroup.markAsPristine(); + } + + saveInfo(): void { + const title: string = this.imageFormGroup.get('title').value; + const image = {...this.image, ...{title}}; + this.imageService.updateImageInfo(image).subscribe( + (saved) => { + this.image = saved; + this.imageChanged = true; + this.imageFormGroup.markAsPristine(); + } + ); + } + + downloadImage($event) { + if ($event) { + $event.stopPropagation(); + } + this.imageService.downloadImage(imageResourceType(this.image), this.image.resourceKey).subscribe(); + } + + exportImage($event) { + if ($event) { + $event.stopPropagation(); + } + this.importExportService.exportImage(imageResourceType(this.image), this.image.resourceKey); + } + + updateImage($event): void { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(UploadImageDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + image: this.image + } + }).afterClosed().subscribe((result) => { + if (result) { + this.imageChanged = true; + this.image = result; + this.imagePreviewData = { + url: this.image.link + }; + } + }); + } + +} diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html new file mode 100644 index 0000000000..882f411dd9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -0,0 +1,377 @@ + +
+
+ +
+
+ image.gallery +
+
+ +
+
+ +
+
+ {{ 'image.include-system-images' | translate }} +
+
+ +
+ + + + +
+ +
+
+ {{ 'image.include-system-images' | translate }} +
+ +
+ + +   + + + +
+
+ +
+ + {{ translate.get('image.selected-images', {count: dataSource?.selection.selected.length}) | async }} + + + +
+
+
+
+ + + + + + + + + + + + + + + {{ image.title }} + + + + {{ 'image.name' | translate }} + + {{ image.title }} + + + + {{ 'image.created-time' | translate }} + + {{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + {{ 'image.resolution' | translate }} + + {{ image.descriptor.width }}x{{ image.descriptor.height }} + + + + {{ 'image.size' | translate }} + + {{ image.descriptor.size | fileSize }} + + + + {{ 'image.system' | translate }} + + {{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
+ + + + +
+
+ + + + + + + +
+
+
+ + + + + + + + +
+ + + + {{ 'common.loading' | translate }} +
+ + +
+
+ + +
+
+
+ +
+ + {{ 'common.loading' | translate }} + + +
+
+ +
+
+
image.no-images
+
+
+ +
+
+
+ + + +
+
+
+
+ + +
+
+ {{ item.title }} +
+
+
+
+ {{ item.title }} +
+
sys
+
+
+
{{ item.descriptor.width }}x{{ item.descriptor.height }}
+ +
{{ item.descriptor.size | fileSize }}
+
+
+
+
+ +
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.scss b/ui-ngx/src/app/shared/components/image/image-gallery.component.scss new file mode 100644 index 0000000000..83dd278e4b --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.scss @@ -0,0 +1,295 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../scss/constants"; + +$tb-button-selected-color: rgb(255, 110, 64) !default; + +.tb-images { + + &.tb-popover-mode { + padding-top: 8px; + position: relative; + width: 300px; + height: 500px; + max-width: calc(100vw - 50px); + max-height: calc(100vh - 100px); + @media #{$mat-gt-xs} { + width: 500px; + height: 600px; + } + @media #{$mat-gt-sm} { + width: 700px; + height: 600px; + } + @media #{$mat-gt-md} { + width: 1000px; + height: 700px; + } + @media #{$mat-gt-xmd} { + width: 1300px; + height: 700px; + } + @media #{$mat-gt-xl} { + width: 1600px; + height: 800px; + } + @media screen and (min-width: 2320px) { + width: 2000px; + height: 800px; + } + } + + .tb-images-content { + width: 100%; + height: 100%; + background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } + + .mat-mdc-table-toolbar { + &.multi-row { + &.mat-toolbar-single-row { + height: 112px; + } + .mat-mdc-slide-toggle { + display: flex; + min-height: 48px; + } + } + } + + .tb-images-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .tb-images-view-type-toolbar { + height: 55px; + min-height: 55px; + padding-right: 16px; + background-color: transparent; + box-shadow: none; + + .tb-toolbar-button { + height: 48px; + button.mat-mdc-icon-button { + margin: 0; + } + &.tb-selected { + background-color: rgba(255, 255, 255, .15); + border-bottom: $tb-button-selected-color solid 4px; + + button.mat-mdc-icon-button { + margin-bottom: -4px; + + .mat-icon { + color: $tb-button-selected-color; + fill: $tb-button-selected-color; + } + } + } + } + } + + .table-container, tb-scroll-grid { + position: relative; + } + + .tb-no-images { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .tb-image-card { + position: relative; + height: 100%; + border-radius: 4px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.24); + padding: 8px; + display: flex; + gap: 8px; + flex-direction: column; + cursor: pointer; + + .tb-image-card-overlay { + position: absolute; + pointer-events: none; + inset: 0; + border-radius: 4px; + z-index: 2; + display: flex; + flex-direction: column; + .tb-image-card-overlay-buttons { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + color: rgba(0,0,0,0.78); + opacity: 0; + transition: opacity 0.5s; + } + } + + &:hover { + .tb-image-card-overlay { + .tb-image-card-overlay-buttons { + opacity: 1; + } + } + .tb-image-preview-container { + .tb-image-preview-overlay { + background: rgba(245,245,245,0.6); + backdrop-filter: blur(4px); + .mdc-button { + opacity: 1; + } + } + } + } + + .tb-image-preview-container { + position: relative; + .tb-image-preview-overlay { + position: absolute; + inset: 0; + z-index: 1; + background: rgba(245,245,245,0); + backdrop-filter: none; + transition: all 0.5s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .mdc-button { + opacity: 0; + transition: opacity 0.5s; + } + } + .tb-image-preview-spacer { + margin-top: 100%; + } + .tb-image-preview { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + } + } + + .tb-image-details { + min-height: 52px; + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 4px; + align-self: stretch; + .tb-image-title-container { + display: flex; + align-items: center; + gap: 8px; + align-self: stretch; + .tb-image-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + color: rgba(0, 0, 0, 0.76); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + } + .tb-image-sys { + padding: 1px 4px; + border-radius: 4px; + background: rgba(236, 236, 236, 0.64); + color: rgba(0, 0, 0, 0.54); + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.017px; + } + } + .tb-image-info-container { + display: flex; + align-items: center; + gap: 8px; + align-self: stretch; + color: rgba(0, 0, 0, 0.38); + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 16px; + .mat-divider.mat-divider-vertical { + height: 16px; + } + } + } + &.loading-cell { + .tb-image-preview-container, .tb-image-details { + background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%); + border-radius: 5px; + background-size: 200% 100%; + animation: 1s shine linear infinite; + } + } + } + + .table-container { + overflow: auto; + + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + .mat-mdc-cell.mat-column-preview { + width: 50px; + height: 50px; + padding: 2px 12px; + display: block; + } + } + + .tb-image-preview { + width: 50px; + height: 50px; + object-fit: contain; + border-radius: 4px; + } + } +} + +@keyframes shine { + to { + background-position-x: -200%; + } +} diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts new file mode 100644 index 0000000000..af223e8563 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -0,0 +1,670 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ImageResourceInfo, + ImageResourceInfoWithReferences, + imageResourceType, + toImageDeleteResult +} from '@shared/models/resource.models'; +import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs'; +import { ImageService } from '@core/http/image.service'; +import { TranslateService } from '@ngx-translate/core'; +import { PageLink, PageQueryParam } from '@shared/models/page/page-link'; +import { catchError, debounceTime, distinctUntilChanged, map, skip, takeUntil } from 'rxjs/operators'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, EventEmitter, + Input, + OnDestroy, + OnInit, Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogService } from '@core/services/dialog.service'; +import { FormBuilder } from '@angular/forms'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; +import { isEqual, isNotEmptyStr, parseHttpErrorMessage } from '@core/utils'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { GridEntitiesFetchFunction, ScrollGridColumns } from '@shared/components/grid/scroll-grid-datasource'; +import { ItemSizeStrategy, ScrollGridComponent } from '@shared/components/grid/scroll-grid.component'; +import { MatDialog } from '@angular/material/dialog'; +import { + UploadImageDialogComponent, + UploadImageDialogData +} from '@shared/components/image/upload-image-dialog.component'; +import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/image-dialog.component'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { + ImagesInUseDialogComponent, + ImagesInUseDialogData +} from '@shared/components/image/images-in-use-dialog.component'; +import { ImagesDatasource } from '@shared/components/image/images-datasource'; + +interface GridImagesFilter { + search: string; + includeSystemImages: boolean; +} + +const pageGridColumns: ScrollGridColumns = { + columns: 2, + breakpoints: { + 'screen and (min-width: 2320px)': 10, + 'screen and (min-width: 2000px)': 8, + 'gt-lg': 7, + 'screen and (min-width: 1600px)': 6, + 'gt-md': 5, + 'screen and (min-width: 1120px)': 4, + 'gt-xs': 3 + } +}; + +const popoverGridColumns: ScrollGridColumns = { + columns: 1, + breakpoints: { + 'screen and (min-width: 2320px)': 8, + 'gt-lg': 6, + 'screen and (min-width: 1600px)': 5, + 'gt-md': 4, + 'gt-sm': 3, + 'gt-xs': 2 + } +}; + +@Component({ + selector: 'tb-image-gallery', + templateUrl: './image-gallery.component.html', + styleUrls: ['./image-gallery.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ImageGalleryComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit { + + @Input() + @coerceBoolean() + pageMode = true; + + @Input() + @coerceBoolean() + popoverMode = false; + + @Input() + mode: 'list' | 'grid' = 'list'; + + @Input() + selectionMode = false; + + @Output() + imageSelected = new EventEmitter(); + + @ViewChild('searchInput') searchInputField: ElementRef; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + @ViewChild(ScrollGridComponent) gridComponent: ScrollGridComponent; + + defaultPageSize = 10; + defaultSortOrder: SortOrder = { property: 'createdTime', direction: Direction.DESC }; + hidePageSize = false; + + displayedColumns: string[]; + pageSizeOptions: number[]; + pageLink: PageLink; + + textSearchMode = false; + + dataSource: ImagesDatasource; + + textSearch = this.fb.control('', {nonNullable: true}); + includeSystemImages = false; + + gridColumns: ScrollGridColumns; + + gridImagesFetchFunction: GridEntitiesFetchFunction; + gridImagesFilter: GridImagesFilter = { + search: '', + includeSystemImages: false + }; + + gridImagesItemSizeStrategy: ItemSizeStrategy = { + defaultItemSize: 200, + itemSizeFunction: itemWidth => itemWidth + 72 + }; + + authUser = getCurrentAuthUser(this.store); + + private updateDataSubscription: Subscription; + + private widgetResize$: ResizeObserver; + private destroy$ = new Subject(); + private destroyListMode$: Subject; + + constructor(protected store: Store, + private route: ActivatedRoute, + private router: Router, + private dialog: MatDialog, + public translate: TranslateService, + private imageService: ImageService, + private dialogService: DialogService, + private importExportService: ImportExportService, + private elementRef: ElementRef, + private cd: ChangeDetectorRef, + private fb: FormBuilder) { + super(store); + + this.gridImagesFetchFunction = (pageSize, page, filter) => { + const pageLink = new PageLink(pageSize, page, filter.search, { + property: 'createdTime', + direction: Direction.DESC + }); + return this.imageService.getImages(pageLink, filter.includeSystemImages); + }; + } + + ngOnInit(): void { + this.gridColumns = this.popoverMode ? popoverGridColumns : pageGridColumns; + this.displayedColumns = this.computeDisplayedColumns(); + let sortOrder: SortOrder = this.defaultSortOrder; + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams; + if (this.pageMode) { + if (routerQueryParams.hasOwnProperty('direction') + || routerQueryParams.hasOwnProperty('property')) { + sortOrder = { + property: routerQueryParams?.property || this.defaultSortOrder.property, + direction: routerQueryParams?.direction || this.defaultSortOrder.direction + }; + } + } + this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); + if (this.pageMode) { + if (routerQueryParams.hasOwnProperty('page')) { + this.pageLink.page = Number(routerQueryParams.page); + } + if (routerQueryParams.hasOwnProperty('pageSize')) { + this.pageLink.pageSize = Number(routerQueryParams.pageSize); + } + const textSearchParam = routerQueryParams.textSearch; + if (isNotEmptyStr(textSearchParam)) { + const decodedTextSearch = decodeURI(textSearchParam); + this.textSearchMode = true; + this.pageLink.textSearch = decodedTextSearch.trim(); + this.textSearch.setValue(decodedTextSearch, {emitEvent: false}); + } + } + if (this.mode === 'list') { + this.dataSource = new ImagesDatasource(this.imageService, null, + entity => this.deleteEnabled(entity)); + } + } + + ngOnDestroy(): void { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } + if (this.destroyListMode$) { + this.destroyListMode$.next(); + this.destroyListMode$.complete(); + } + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + this.textSearch.valueChanges.pipe( + debounceTime(150), + distinctUntilChanged((prev, current) => + ((this.mode === 'list' ? this.pageLink.textSearch : this.gridImagesFilter.search) ?? '') === current.trim()), + takeUntil(this.destroy$) + ).subscribe(value => { + if (this.mode === 'list') { + if (this.pageMode) { + const queryParams: PageQueryParam = { + textSearch: isNotEmptyStr(value) ? encodeURI(value) : null, + page: null + }; + this.updatedRouterParamsAndData(queryParams); + } else { + this.pageLink.textSearch = isNotEmptyStr(value) ? value.trim() : null; + this.paginator.pageIndex = 0; + this.updateData(); + } + } else { + this.gridImagesFilter = { + search: isNotEmptyStr(value) ? value.trim() : null, + includeSystemImages: this.includeSystemImages + }; + this.cd.markForCheck(); + } + }); + this.updateMode(); + } + + public includeSystemImagesChanged(value: boolean) { + this.includeSystemImages = value; + this.displayedColumns = this.computeDisplayedColumns(); + this.gridImagesFilter = { + search: this.gridImagesFilter.search, + includeSystemImages: this.includeSystemImages + }; + if (this.mode === 'list') { + this.paginator.pageIndex = 0; + this.updateData(); + } else { + this.cd.markForCheck(); + } + } + + public setMode(targetMode: 'list' | 'grid') { + if (this.mode !== targetMode) { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + this.widgetResize$ = null; + } + if (this.destroyListMode$) { + this.destroyListMode$.next(); + this.destroyListMode$.complete(); + this.destroyListMode$ = null; + } + this.mode = targetMode; + if (this.mode === 'list') { + this.dataSource = new ImagesDatasource(this.imageService, null, + entity => this.deleteEnabled(entity)); + } + setTimeout(() => { + this.updateMode(); + }); + } + } + + public get isSysAdmin(): boolean { + return this.authUser.authority === Authority.SYS_ADMIN; + } + + private computeDisplayedColumns(): string[] { + let columns: string[]; + if (this.selectionMode) { + columns = ['preview', 'title']; + if (!this.isSysAdmin && this.includeSystemImages) { + columns.push('system'); + } + columns.push('imageSelect'); + } else { + columns = ['select', 'preview', 'title', 'createdTime', 'resolution', 'size']; + if (!this.isSysAdmin && this.includeSystemImages) { + columns.push('system'); + } + columns.push('actions'); + } + return columns; + } + + private updateMode() { + if (this.mode === 'list') { + this.initListMode(); + } else { + this.initGridMode(); + } + } + + private initListMode() { + this.destroyListMode$ = new Subject(); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.markForCheck(); + } + }); + this.widgetResize$.observe(this.elementRef.nativeElement); + if (this.pageMode) { + this.route.queryParams.pipe( + skip(1), + takeUntil(this.destroyListMode$) + ).subscribe((params: PageQueryParam) => { + this.paginator.pageIndex = Number(params.page) || 0; + this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize; + this.sort.active = params.property || this.defaultSortOrder.property; + this.sort.direction = (params.direction || this.defaultSortOrder.direction).toLowerCase() as SortDirection; + const textSearchParam = params.textSearch; + if (isNotEmptyStr(textSearchParam)) { + const decodedTextSearch = decodeURI(textSearchParam); + this.textSearchMode = true; + this.pageLink.textSearch = decodedTextSearch.trim(); + this.textSearch.setValue(decodedTextSearch, {emitEvent: false}); + } else { + this.pageLink.textSearch = null; + this.textSearch.reset('', {emitEvent: false}); + } + this.updateData(); + }); + } + this.updatePaginationSubscriptions(); + this.updateData(); + } + + private initGridMode() { + + } + + private updatePaginationSubscriptions() { + if (this.updateDataSubscription) { + this.updateDataSubscription.unsubscribe(); + this.updateDataSubscription = null; + } + const sortSubscription$: Observable = this.sort.sortChange.asObservable().pipe( + map((data) => { + const direction = data.direction.toUpperCase(); + const queryParams: PageQueryParam = { + direction: (this.defaultSortOrder.direction === direction ? null : direction) as Direction, + property: this.defaultSortOrder.property === data.active ? null : data.active + }; + queryParams.page = null; + this.paginator.pageIndex = 0; + return queryParams; + }) + ); + const paginatorSubscription$ = this.paginator.page.asObservable().pipe( + map((data) => ({ + page: data.pageIndex === 0 ? null : data.pageIndex, + pageSize: data.pageSize === this.defaultPageSize ? null : data.pageSize + })) + ); + this.updateDataSubscription = (merge(sortSubscription$, paginatorSubscription$) as Observable).pipe( + takeUntil(this.destroyListMode$) + ).subscribe(queryParams => this.updatedRouterParamsAndData(queryParams)); + } + + clearSelection() { + this.dataSource.selection.clear(); + this.cd.detectChanges(); + } + + updateData() { + if (this.mode === 'list') { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + if (this.sort.active) { + this.pageLink.sortOrder = { + property: this.sort.active, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } + this.dataSource.loadEntities(this.pageLink, this.includeSystemImages); + } else { + this.gridComponent.update(); + } + } + + private imageUpdated(image: ImageResourceInfo, index = -1) { + if (this.mode === 'list') { + this.updateData(); + } else { + this.gridComponent.updateItem(index, image); + } + } + + private imageDeleted(index = -1) { + if (this.mode === 'list') { + this.updateData(); + } else { + this.gridComponent.deleteItem(index); + } + } + + enterFilterMode() { + this.textSearchMode = true; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch.reset(); + } + + trackByEntity(index: number, entity: BaseData) { + return entity; + } + + isSystem(image?: ImageResourceInfo): boolean { + return !this.isSysAdmin && image?.tenantId?.id === NULL_UUID; + } + + readonly(image?: ImageResourceInfo): boolean { + return this.authUser.authority !== Authority.SYS_ADMIN && this.isSystem(image); + } + + deleteEnabled(image?: ImageResourceInfo): boolean { + return this.authUser.authority === Authority.SYS_ADMIN || !this.isSystem(image); + } + + deleteImage($event: Event, image: ImageResourceInfo, itemIndex = -1) { + if ($event) { + $event.stopPropagation(); + } + const title = this.translate.instant('image.delete-image-title', {imageTitle: image.title}); + const content = this.translate.instant('image.delete-image-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( + map(() => toImageDeleteResult(image)), + catchError((err) => of(toImageDeleteResult(image, err))) + ).subscribe( + (deleteResult) => { + if (deleteResult.success) { + this.imageDeleted(itemIndex); + } else if (deleteResult.imageIsReferencedError) { + this.dialog.open(ImagesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + multiple: false, + images: [{...image, ...{references: deleteResult.references}}] + } + }).afterClosed().subscribe((images) => { + if (images) { + this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe( + () => { + this.imageDeleted(itemIndex); + } + ); + } + }); + } else { + const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + }); + } + }); + } + + deleteImages($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const selectedImages = this.dataSource.selection.selected; + if (selectedImages && selectedImages.length) { + const title = this.translate.instant('image.delete-images-title', {count: selectedImages.length}); + const content = this.translate.instant('image.delete-images-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + const tasks = selectedImages.map((image) => + this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( + map(() => toImageDeleteResult(image)), + catchError((err) => of(toImageDeleteResult(image, err))) + ) + ); + forkJoin(tasks).subscribe( + (deleteResults) => { + const anySuccess = deleteResults.some(res => res.success); + const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError); + const otherError = deleteResults.find(res => !res.success); + if (anySuccess) { + this.updateData(); + } + if (referenceErrors?.length) { + const imagesWithReferences: ImageResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}})); + this.dialog.open(ImagesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + multiple: true, + images: imagesWithReferences + } + }).afterClosed().subscribe((forceDeleteImages) => { + if (forceDeleteImages && forceDeleteImages.length) { + const forceDeleteTasks = forceDeleteImages.map((image) => + this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true) + ); + forkJoin(forceDeleteTasks).subscribe( + () => { + this.updateData(); + } + ); + } + }); + } else if (otherError) { + const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + } + + downloadImage($event, image: ImageResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.imageService.downloadImage(imageResourceType(image), image.resourceKey).subscribe(); + } + + exportImage($event, image: ImageResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.importExportService.exportImage(imageResourceType(image), image.resourceKey); + } + + importImage(): void { + this.importExportService.importImage().subscribe((image) => { + if (image) { + this.updateData(); + } + }); + } + + selectImage($event, image: ImageResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.imageSelected.next(image); + } + + rowClick($event, image: ImageResourceInfo) { + if (this.selectionMode) { + this.selectImage($event, image); + } else { + this.dataSource.selection.toggle(image); + } + } + + uploadImage(): void { + this.dialog.open(UploadImageDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed().subscribe((result) => { + if (result) { + if (this.selectionMode) { + this.imageSelected.next(result); + } else { + this.updateData(); + } + } + }); + } + + editImage($event: Event, image: ImageResourceInfo, itemIndex = -1) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(ImageDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + image, + readonly: this.readonly(image) + } + }).afterClosed().subscribe((result) => { + if (result) { + this.imageUpdated(result, itemIndex); + } + }); + } + + protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { + if (this.pageMode) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling + }); + if (queryParamsHandling === '' && isEqual(this.route.snapshot.queryParams, queryParams)) { + this.updateData(); + } + } else { + this.updateData(); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.html b/ui-ngx/src/app/shared/components/image/image-references.component.html new file mode 100644 index 0000000000..611d744cfa --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-references.component.html @@ -0,0 +1,54 @@ + + + + + + +
    + +
  • +
    {{ 'image.system-entities' | translate }}
    + +
  • + +
  • +
    + {{ 'tenant.tenant' | translate }} {{ entry[1].tenantName }} {{ 'image.entities' | translate }} +
    + +
  • +
    +
    +
+
+
+ + + + + + +
{{ referencedEntity.typeName }} + {{ referencedEntity.entity.name }} + {{ referencedEntity.entity.name }} +
+
+ + + diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.scss b/ui-ngx/src/app/shared/components/image/image-references.component.scss new file mode 100644 index 0000000000..4f69db0add --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-references.component.scss @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../scss/constants"; + +:host { + ul.tb-references { + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + } + li.tb-entities-container { + padding: 8px 12px 12px 12px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + border-radius: 8px; + } + a.tb-reference { + color: $tb-primary-color; + font-weight: inherit; + } + .tb-entities-title { + display: flex; + gap: 8px; + color: rgba(0, 0, 0, 0.87); + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + } + table.tb-entities-list-table { + position: relative; + overflow: hidden; + padding: 8px 10px 8px 4px; + border-radius: 4px; + background: #fff; + align-self: stretch; + z-index: 1; + color: rgba(0, 0, 0, 0.76); + &:before { + display: block; + height: auto; + content: ""; + position: absolute; + inset: 0; + border-radius: 4px; + border: 1px solid $tb-primary-color; + background: transparent; + opacity: 0.4; + pointer-events: none; + } + td.tb-entity-type { + white-space: nowrap; + padding-right: 20px; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + &:before { + content: "•"; + padding-left: 8px; + padding-right: 8px; + } + } + td.tb-entity-name { + width: 100%; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.2px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.ts b/ui-ngx/src/app/shared/components/image/image-references.component.ts new file mode 100644 index 0000000000..b236c6d47c --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/image-references.component.ts @@ -0,0 +1,171 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ImageReferences } from '@shared/models/resource.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { getEntityDetailsPageURL } from '@core/utils'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { Authority } from '@shared/models/authority.enum'; +import { Observable } from 'rxjs'; +import { EntityService } from '@core/http/entity.service'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { HasTenantId } from '@shared/models/entity.models'; +import { map } from 'rxjs/operators'; +import { TbPopoverComponent } from '@shared/components/popover.component'; + +interface ReferencedEntityInfo { + entity: BaseData & HasTenantId; + typeName: string; + detailsUrl: string; +} + +interface TenantReferencedEntities { + tenantName?: string; + tenantDetailsUrl?: string; + entities: ReferencedEntityInfo[]; +} + +type ReferencedEntities = {[tenantId: string]: TenantReferencedEntities}; +type ReferencedEntitiesEntry = [string, TenantReferencedEntities]; + +@Component({ + selector: 'tb-image-references', + templateUrl: './image-references.component.html', + styleUrls: ['./image-references.component.scss'] +}) +export class ImageReferencesComponent implements OnInit { + + @Input() + references: ImageReferences; + + popoverComponent: TbPopoverComponent; + + contentReady = false; + + authUser = getCurrentAuthUser(this.store); + + simpleList = true; + + referencedEntitiesList: ReferencedEntityInfo[]; + + referencedEntitiesEntries: ReferencedEntitiesEntry[]; + + constructor(protected store: Store, + private entityService: EntityService, + private cd: ChangeDetectorRef, + private translate: TranslateService) { + } + + ngOnInit(): void { + if (this.authUser.authority === Authority.SYS_ADMIN && this.hasNonSystemEntities(this.references)) { + this.simpleList = false; + this.toReferencedEntitiesEntries(this.references).subscribe( + (entries) => { + this.referencedEntitiesEntries = entries; + this.contentReady = true; + this.cd.detectChanges(); + if (this.popoverComponent) { + Promise.resolve().then(() => { + this.popoverComponent.updatePosition(); + }); + } + } + ); + } else { + this.referencedEntitiesList = this.toReferencedEntitiesList(this.references); + this.contentReady = true; + } + } + + isSystem(tenantId: string): boolean { + return tenantId === NULL_UUID; + } + + private hasNonSystemEntities(references: ImageReferences): boolean { + for (const entityTypeStr of Object.keys(references)) { + const entities = this.references[entityTypeStr]; + if (entities.some(e => e.tenantId && e.tenantId.id && e.tenantId.id !== NULL_UUID)) { + return true; + } + } + return false; + } + + private toReferencedEntitiesList(references: ImageReferences): ReferencedEntityInfo[] { + const result: ReferencedEntityInfo[] = []; + for (const entityTypeStr of Object.keys(references)) { + const entityType = entityTypeStr as EntityType; + const entityTypeName = this.translate.instant(entityTypeTranslations.get(entityType).type); + const entities = references[entityTypeStr]; + for (const entity of entities) { + const detailsUrl = getEntityDetailsPageURL(entity.id.id, entityType); + result.push({ + entity, + typeName: entityTypeName, + detailsUrl + }); + } + } + return result; + } + + private toReferencedEntitiesEntries(references: ImageReferences): Observable { + let referencedEntities: ReferencedEntities = {}; + const referencedEntitiesList = this.toReferencedEntitiesList(references); + for (const referencedEntityInfo of referencedEntitiesList) { + const tenantId = referencedEntityInfo.entity.tenantId?.id || NULL_UUID; + let tenantEntitiesInfo = referencedEntities[tenantId]; + if (!tenantEntitiesInfo) { + tenantEntitiesInfo = { + entities: [] + }; + referencedEntities[tenantId] = tenantEntitiesInfo; + } + tenantEntitiesInfo.entities.push(referencedEntityInfo); + } + referencedEntities = Object.keys(referencedEntities).sort((tenantId1, tenantId2) => { + if (tenantId1 === NULL_UUID) { + return -1; + } else if (tenantId2 === NULL_UUID) { + return 1; + } + return 0; + }).reduce( + (obj, key) => { + obj[key] = referencedEntities[key]; + return obj; + }, + {} + ); + const tenantIds = Object.keys(referencedEntities).filter(id => id !== NULL_UUID); + return this.entityService.getEntities(EntityType.TENANT, tenantIds).pipe( + map((tenants) => { + for (const tenant of tenants) { + const tenantEntitiesInfo = referencedEntities[tenant.id.id]; + tenantEntitiesInfo.tenantName = tenant.name; + tenantEntitiesInfo.tenantDetailsUrl = getEntityDetailsPageURL(tenant.id.id, EntityType.TENANT); + } + return Object.entries(referencedEntities); + }) + ); + } + +} diff --git a/ui-ngx/src/app/shared/components/image/images-datasource.ts b/ui-ngx/src/app/shared/components/image/images-datasource.ts new file mode 100644 index 0000000000..8be2f30cc0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/images-datasource.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { ImageResourceInfo } from '@shared/models/resource.models'; +import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { ImageService } from '@core/http/image.service'; +import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, take, tap } from 'rxjs/operators'; + +export class ImagesDatasource implements DataSource { + private entitiesSubject: Subject; + private readonly pageDataSubject: Subject>; + + public pageData$: Observable>; + + public selection = new SelectionModel(true, []); + + public dataLoading = true; + + constructor(private imageService: ImageService, + private images: ImageResourceInfo[], + private selectionEnabledFunction: EntityBooleanFunction) { + if (this.images && this.images.length) { + this.entitiesSubject = new BehaviorSubject(this.images); + } else { + this.entitiesSubject = new BehaviorSubject([]); + this.pageDataSubject = new BehaviorSubject>(emptyPageData()); + this.pageData$ = this.pageDataSubject.asObservable(); + } + } + + connect(collectionViewer: CollectionViewer): + Observable> { + return this.entitiesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.entitiesSubject.complete(); + if (this.pageDataSubject) { + this.pageDataSubject.complete(); + } + } + + reset() { + this.entitiesSubject.next([]); + if (this.pageDataSubject) { + this.pageDataSubject.next(emptyPageData()); + } + } + + loadEntities(pageLink: PageLink, includeSystemImages = false): Observable> { + this.dataLoading = true; + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, includeSystemImages).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.entitiesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, includeSystemImages = false): Observable> { + return this.imageService.getImages(pageLink, includeSystemImages); + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.entitiesSubject.pipe( + map((entities) => numSelected === entities.length) + ); + } + + isEmpty(): Observable { + return this.entitiesSubject.pipe( + map((entities) => !entities.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + masterToggle() { + this.entitiesSubject.pipe( + tap((entities) => { + const numSelected = this.selection.selected.length; + if (numSelected === this.selectableEntitiesCount(entities)) { + this.selection.clear(); + } else { + entities.forEach(row => { + if (this.selectionEnabledFunction(row)) { + this.selection.select(row); + } + }); + } + }), + take(1) + ).subscribe(); + } + + private selectableEntitiesCount(entities: Array): number { + return entities.filter((entity) => this.selectionEnabledFunction(entity)).length; + } +} diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html new file mode 100644 index 0000000000..8739f7c9b2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html @@ -0,0 +1,82 @@ + +

{{title}}

+
+
+ +
+ + + + + + + + + + + + + + + {{ image.title }} + + + + + + {{ 'image.name' | translate }} + + + {{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }} + + + + {{ image.title }} + + + + + + + + + + +
+
+
+
+
+ + +
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss new file mode 100644 index 0000000000..ba8531802e --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-images-in-use-content { + display: flex; + flex-direction: column; + gap: 24px; + &.multiple { + gap: 16px; + } + .table-container { + overflow: auto; + .mat-mdc-cell.mat-column-preview { + width: 50px; + height: 50px; + padding: 2px 12px; + display: block; + } + } + .tb-image-preview { + width: 50px; + height: 50px; + object-fit: contain; + border-radius: 4px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts new file mode 100644 index 0000000000..ccb530b642 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { ImageReferences, ImageResourceInfo, ImageResourceInfoWithReferences } from '@shared/models/resource.models'; +import { ImagesDatasource } from '@shared/components/image/images-datasource'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; +import { TranslateService } from '@ngx-translate/core'; + +export interface ImagesInUseDialogData { + multiple: boolean; + images: ImageResourceInfoWithReferences[]; +} + +@Component({ + selector: 'tb-images-in-use-dialog', + templateUrl: './images-in-use-dialog.component.html', + styleUrls: ['./images-in-use-dialog.component.scss'] +}) +export class ImagesInUseDialogComponent extends + DialogComponent implements OnInit { + + title: string; + message: string; + + references: ImageReferences; + + dataSource: ImagesDatasource; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ImagesInUseDialogData, + public dialogRef: MatDialogRef, + public translate: TranslateService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private popoverService: TbPopoverService) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + if (this.data.multiple) { + this.title = this.translate.instant('image.images-are-in-use'); + this.message = this.translate.instant('image.images-are-in-use-text'); + this.dataSource = new ImagesDatasource(null, this.data.images, entity => true); + } else { + this.title = this.translate.instant('image.image-is-in-use'); + this.message = this.translate.instant('image.image-is-in-use-text', {title: this.data.images[0].title}); + this.references = this.data.images[0].references; + } + } + + cancel() { + this.dialogRef.close(null); + } + + delete() { + if (this.data.multiple) { + this.dialogRef.close(this.dataSource.selection.selected); + } else { + this.dialogRef.close(this.data.images); + } + } + + toggleShowReferences($event: Event, image: ImageResourceInfoWithReferences, referencesButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = referencesButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const referencesPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ImageReferencesComponent, 'top', true, null, + { + references: image.references + }, {}, {}, {}, + false, + visible => { + const addClasses = + visible ? 'mdc-button--unelevated mat-mdc-unelevated-button' : 'mdc-button--outlined mat-mdc-outlined-button'; + const removeClasses = + visible ? 'mdc-button--outlined mat-mdc-outlined-button' : 'mdc-button--unelevated mat-mdc-unelevated-button'; + addClasses.split(' ').forEach(clazz => { + this.renderer.addClass(trigger, clazz); + }); + removeClasses.split(' ').forEach(clazz => { + this.renderer.removeClass(trigger, clazz); + }); + }); + referencesPopover.tbComponentRef.instance.popoverComponent = referencesPopover; + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html new file mode 100644 index 0000000000..aeae4d9a1f --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.html @@ -0,0 +1,111 @@ + +
+ +
+
+
+ {{ 'image-input.images' | translate }} [{{ $index }}] +
+
+ drag_indicator +
+
+ +
+
+ +
+
+
+
{{ 'image-input.no-images' | translate }}
+
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
{{ 'image.no-image-selected' | translate }}
+
diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.scss b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.scss new file mode 100644 index 0000000000..a0cf328630 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.scss @@ -0,0 +1,225 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../scss/constants"; + +$imagesContainerHeight: 106px !default; +$selectContainerHeight: 96px !default; +$previewSize: 64px !default; + +.image-card { + margin-bottom: 8px; + &.image-dnd-placeholder { + height: 82px; + width: 146px; + border: 2px dashed rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + } + &.image-dragging { + display: none !important; + } + .image-title { + font-size: 11px; + font-weight: 400; + line-height: 14px; + color: rgba(0, 0, 0, 0.6); + padding-bottom: 4px; + } + + .image-content-container { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; + height: $previewSize; + } + + .tb-image-preview { + width: auto; + max-width: $previewSize - 2px; + height: auto; + max-height: $previewSize - 2px; + } + + .tb-image-preview-container { + position: relative; + width: $previewSize; + height: $previewSize; + margin-top: -1px; + margin-bottom: -1px; + border: 1px solid rgba(0, 0, 0, 0.54); + + .tb-image-preview { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .tb-image-action-container { + position: relative; + height: $previewSize - 2px; + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + } +} + +:host { + .tb-container { + margin-top: 0; + padding: 0 0 16px; + display: flex; + flex-direction: column; + gap: 8px; + label.tb-title { + display: block; + padding-bottom: 0; + } + } + + .images-container { + padding: 12px 12px 4px; + background: rgba(0, 0, 0, 0.03); + border-radius: 4px; + flex-wrap: wrap; + margin-bottom: 8px; + &.no-images { + height: $imagesContainerHeight; + padding-bottom: 12px; + align-items: center; + justify-content: center; + } + } + + .no-images-prompt { + font-size: 18px; + color: rgba(0, 0, 0, 0.54); + } + + .tb-image-select-container { + width: 100%; + height: $selectContainerHeight; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + align-items: center; + + .tb-image-container { + width: $selectContainerHeight - 1px; + height: $selectContainerHeight - 2px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + border-right: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + overflow: hidden; + + .tb-image-preview { + width: auto; + max-width: $selectContainerHeight - 2px; + height: auto; + max-height: $selectContainerHeight - 2px; + } + + .tb-no-image { + text-align: center; + color: rgba(0, 0, 0, 0.38); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + } + + .tb-image-info-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 0 8px; + justify-content: flex-end; + align-items: center; + gap: 4px; + + .tb-external-image-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 16px 8px 0 16px; + flex-direction: column; + align-items: flex-start; + gap: 4px; + .tb-external-link-label { + color: rgba(0, 0, 0, 0.54); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + } + .tb-external-link-input-container { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 4px; + align-self: stretch; + .tb-inline-field { + width: 100%; + } + .tb-image-decline-btn { + color: rgba(0,0,0,0.38); + } + } + } + + } + + .tb-image-select-buttons-container { + display: flex; + flex: 1; + align-self: stretch; + padding: 8px; + gap: 8px; + justify-content: center; + align-items: flex-start; + .tb-image-select-button { + width: 100%; + height: 100%; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + padding: 8px; + line-height: normal; + font-size: 12px; + @media #{$mat-gt-xs} { + padding: 16px; + } + .mat-icon { + width: 24px; + height: 24px; + font-size: 24px; + margin: 0; + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts new file mode 100644 index 0000000000..5a6a4e7d37 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts @@ -0,0 +1,181 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { DndDropEvent } from 'ngx-drag-drop'; +import { isUndefined } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ImageLinkType } from '@shared/components/image/gallery-image-input.component'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MatButton } from '@angular/material/button'; +import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; +import { prependTbImagePrefixToUrls, removeTbImagePrefixFromUrls } from '@shared/models/resource.models'; + +@Component({ + selector: 'tb-multiple-gallery-image-input', + templateUrl: './multiple-gallery-image-input.component.html', + styleUrls: ['./multiple-gallery-image-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MultipleGalleryImageInputComponent), + multi: true + } + ] +}) +export class MultipleGalleryImageInputComponent extends PageComponent implements OnDestroy, ControlValueAccessor { + + @Input() + label: string; + + @Input() + @coerceBoolean() + required = false; + + @Input() + disabled: boolean; + + imageUrls: string[]; + + ImageLinkType = ImageLinkType; + + linkType: ImageLinkType = ImageLinkType.none; + + externalLinkControl = new FormControl(null); + + dragIndex: number; + + private propagateChange = null; + + constructor(protected store: Store, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private popoverService: TbPopoverService) { + super(store); + } + + ngOnDestroy() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string[]): void { + this.reset(); + this.imageUrls = removeTbImagePrefixFromUrls(value); + } + + private updateModel() { + this.cd.markForCheck(); + this.propagateChange(prependTbImagePrefixToUrls(this.imageUrls)); + } + + private reset() { + this.linkType = ImageLinkType.none; + this.externalLinkControl.setValue(null, {emitEvent: false}); + } + + clearImage(index: number) { + this.imageUrls.splice(index, 1); + this.updateModel(); + } + + setLink($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.linkType = ImageLinkType.external; + } + + declineLink($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.reset(); + } + + applyLink($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.imageUrls.push(this.externalLinkControl.value); + this.reset(); + this.updateModel(); + } + + toggleGallery($event: Event, browseGalleryButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = browseGalleryButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + pageMode: false, + popoverMode: true, + mode: 'grid', + selectionMode: true + }; + const imageGalleryPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ImageGalleryComponent, 'top', true, null, + ctx, + {}, + {}, {}, true); + imageGalleryPopover.tbComponentRef.instance.imageSelected.subscribe((image) => { + imageGalleryPopover.hide(); + this.imageUrls.push(image.link); + this.updateModel(); + }); + } + } + + imageDragStart(index: number) { + setTimeout(() => { + this.dragIndex = index; + this.cd.markForCheck(); + }); + } + + imageDragEnd() { + this.dragIndex = -1; + this.cd.markForCheck(); + } + + imageDrop(event: DndDropEvent) { + let index = event.index; + if (isUndefined(index)) { + index = this.imageUrls.length; + } + moveItemInArray(this.imageUrls, this.dragIndex, index); + this.dragIndex = -1; + this.updateModel(); + } +} diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html new file mode 100644 index 0000000000..7967d50eef --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html @@ -0,0 +1,64 @@ + +
+ +

{{ ( uploadImage ? 'image.upload-image' : 'image.update-image' ) | translate }}

+ + +
+ + +
+
+
+ + + + image.name + + + {{ 'image.name-required' | translate }} + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts new file mode 100644 index 0000000000..841f64c883 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormGroupDirective, + NgForm, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { ImageService } from '@core/http/image.service'; +import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models'; + +export interface UploadImageDialogData { + image?: ImageResourceInfo; +} + +@Component({ + selector: 'tb-upload-image-dialog', + templateUrl: './upload-image-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: UploadImageDialogComponent}], + styleUrls: [] +}) +export class UploadImageDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + uploadImageFormGroup: UntypedFormGroup; + + uploadImage = true; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + private imageService: ImageService, + @Inject(MAT_DIALOG_DATA) public data: UploadImageDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.uploadImage = !this.data?.image; + this.uploadImageFormGroup = this.fb.group({ + file: [this.data?.image?.link, [Validators.required]] + }); + if (this.uploadImage) { + this.uploadImageFormGroup.addControl('title', this.fb.control(null, [Validators.required])); + } + } + + imageFileNameChanged(fileName: string) { + if (this.uploadImage) { + const titleControl = this.uploadImageFormGroup.get('title'); + if (!titleControl.value || !titleControl.touched) { + titleControl.setValue(fileName); + } + } + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + upload(): void { + this.submitted = true; + const file: File = this.uploadImageFormGroup.get('file').value; + if (this.uploadImage) { + const title: string = this.uploadImageFormGroup.get('title').value; + this.imageService.uploadImage(file, title).subscribe( + (res) => { + this.dialogRef.close(res); + } + ); + } else { + const image = this.data.image; + this.imageService.updateImage(imageResourceType(image), image.resourceKey, file).subscribe( + (res) => { + this.dialogRef.close(res); + } + ); + } + } +} diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 3b9c56574d..bd9df1b898 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -44,7 +44,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange, - ConnectionPositionPair + ConnectionPositionPair, NoopScrollStrategy } from '@angular/cdk/overlay'; import { Subject, Subscription } from 'rxjs'; import { @@ -311,6 +311,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { [cdkConnectedOverlayHasBackdrop]="hasBackdrop" [cdkConnectedOverlayOrigin]="origin" [cdkConnectedOverlayPositions]="positions" + [cdkConnectedOverlayScrollStrategy]="scrollStrategy" [cdkConnectedOverlayOpen]="visible" [cdkConnectedOverlayPush]="true" (overlayOutsideClick)="onClickOutside($event)" @@ -378,6 +379,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { tbAnimationState = 'active'; + tbHideStart = new Subject(); tbVisibleChange = new Subject(); tbAnimationDone = new Subject(); tbComponentChange = new Subject>(); @@ -450,6 +452,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { public dir: Direction = 'ltr'; classMap: { [klass: string]: any } = {}; positions: ConnectionPositionPair[] = [...DEFAULT_POPOVER_POSITIONS]; + scrollStrategy = new NoopScrollStrategy(); private parentScrollSubscription: Subscription = null; private intersectionObserver = new IntersectionObserver((entries) => { if (this.lastIsIntersecting !== entries[0].isIntersecting) { @@ -486,6 +489,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { } this.intersectionObserver.disconnect(); this.intersectionObserver = null; + this.tbHideStart.complete(); this.tbVisibleChange.complete(); this.tbAnimationDone.complete(); this.tbDestroy.next(); @@ -528,6 +532,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { if (!this.tbVisible) { return; } + this.tbHideStart.next(); if (this.parentScrollSubscription) { this.parentScrollSubscription.unsubscribe(); this.parentScrollSubscription = null; diff --git a/ui-ngx/src/app/shared/components/popover.service.ts b/ui-ngx/src/app/shared/components/popover.service.ts index 1bef922ec1..289db7034b 100644 --- a/ui-ngx/src/app/shared/components/popover.service.ts +++ b/ui-ngx/src/app/shared/components/popover.service.ts @@ -65,16 +65,17 @@ export class TbPopoverService { displayPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, componentType: Type, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any, - showCloseButton = true): TbPopoverComponent { + showCloseButton = true, visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent { const componentRef = this.createPopoverRef(hostView); return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, - injector, context, overlayStyle, popoverStyle, style, showCloseButton); + injector, context, overlayStyle, popoverStyle, style, showCloseButton, visibleFn); } displayPopoverWithComponentRef(componentRef: ComponentRef, trigger: Element, renderer: Renderer2, componentType: Type, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, - popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent { + popoverStyle: any = {}, style?: any, showCloseButton = true, + visibleFn: (visible: boolean) => void = () => {}): TbPopoverComponent { const component = componentRef.instance; this.popoverWithTriggers.push({ trigger, @@ -103,7 +104,11 @@ export class TbPopoverService { component.tbDestroy.subscribe(() => { this.removePopoverByComponent(component); }); + component.tbHideStart.subscribe(() => { + visibleFn(false); + }); component.show(); + visibleFn(true); return component; } diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts index 4647fc6bad..6439d455ed 100644 --- a/ui-ngx/src/app/shared/components/public-api.ts +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -29,3 +29,5 @@ export * from './unit-input.component'; export * from './material-icons.component'; export * from './icon.component'; export * from './hint-tooltip-icon.component'; +export * from './grid/scroll-grid-datasource'; +export * from './grid/scroll-grid.component'; diff --git a/ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.html b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.html rename to ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.html diff --git a/ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts rename to ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html rename to ui-ngx/src/app/shared/import-export/import-dialog-csv.component.html diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss rename to ui-ngx/src/app/shared/import-export/import-dialog-csv.component.scss diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts similarity index 96% rename from ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts rename to ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts index 9782f18ae6..eb29f17017 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts @@ -35,9 +35,9 @@ import { CsvToJsonConfig, CsvToJsonResult, ImportEntityColumnType -} from '@home/components/import-export/import-export.models'; -import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { TableColumnsAssignmentComponent } from '@home/components/import-export/table-columns-assignment.component'; +} from '@shared/import-export/import-export.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component'; import { Ace } from 'ace-builds'; import { getAce } from '@shared/models/ace/ace.models'; @@ -68,7 +68,7 @@ export class ImportDialogCsvComponent extends DialogComponent, isSingleWidget: boolean, + customTitle: string, missingEntityAliases: EntityAliases) => Observable; // @dynamic @Injectable() @@ -99,14 +100,45 @@ export class ImportExportService { private deviceService: DeviceService, private assetService: AssetService, private edgeService: EdgeService, + private imageService: ImageService, private utils: UtilsService, private itembuffer: ItemBufferService, private dialog: MatDialog) { } + public exportImage(type: ImageResourceType, key: string) { + this.imageService.exportImage(type, key).subscribe( + { + next: (imageData) => { + const name = imageData.fileName.split('.')[0]; + this.exportToPc(imageData, name); + }, + error: (e) => { + this.handleExportError(e, 'image.export-failed-error'); + } + } + ); + } + + public importImage(): Observable { + return this.openImportDialog('image.import-image', 'image.image-json-file').pipe( + mergeMap((imageData: ImageExportData) => { + if (!this.validateImportedImage(imageData)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('image.invalid-image-json-file-error'), + type: 'error'})); + throw new Error('Invalid image JSON file'); + } else { + return this.imageService.importImage(imageData); + } + }), + catchError(() => of(null)) + ); + } + public exportDashboard(dashboardId: string) { - this.dashboardService.getDashboard(dashboardId).subscribe( + this.dashboardService.exportDashboard(dashboardId).subscribe( (dashboard) => { let name = dashboard.title; name = name.toLowerCase().replace(/\W/g, '_'); @@ -118,7 +150,7 @@ export class ImportExportService { ); } - public importDashboard(): Observable { + public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable { return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe( mergeMap((dashboard: Dashboard) => { if (!this.validateImportedDashboard(dashboard)) { @@ -137,7 +169,7 @@ export class ImportExportService { return this.processEntityAliases(entityAliases, aliasIds).pipe( mergeMap((missingEntityAliases) => { if (Object.keys(missingEntityAliases).length > 0) { - return this.editMissingAliases(this.dashboardUtils.getWidgetsArray(dashboard), + return onEditMissingAliases(this.dashboardUtils.getWidgetsArray(dashboard), false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).pipe( mergeMap((updatedEntityAliases) => { @@ -171,6 +203,7 @@ export class ImportExportService { } public importWidget(dashboard: Dashboard, targetState: string, + onEditMissingAliases: editMissingAliasesFunction, targetLayoutFunction: () => Observable, onAliasesUpdateFunction: () => void, onFiltersUpdateFunction: () => void): Observable { @@ -221,7 +254,7 @@ export class ImportExportService { return this.processEntityAliases(entityAliases, aliasIds).pipe( mergeMap((missingEntityAliases) => { if (Object.keys(missingEntityAliases).length > 0) { - return this.editMissingAliases([widget], + onEditMissingAliases([widget], false, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).pipe( mergeMap((updatedEntityAliases) => { @@ -264,7 +297,7 @@ export class ImportExportService { } public exportWidgetType(widgetTypeId: string) { - this.widgetService.getWidgetTypeById(widgetTypeId).subscribe( + this.widgetService.exportWidgetType(widgetTypeId).subscribe( (widgetTypeDetails) => { let name = widgetTypeDetails.name; name = name.toLowerCase().replace(/\W/g, '_'); @@ -279,7 +312,7 @@ export class ImportExportService { public exportWidgetTypes(widgetTypeIds: string[]): Observable { const widgetTypesObservables: Array> = []; for (const id of widgetTypeIds) { - widgetTypesObservables.push(this.widgetService.getWidgetTypeById(id)); + widgetTypesObservables.push(this.widgetService.exportWidgetType(id)); } return forkJoin(widgetTypesObservables).pipe( map((widgetTypes) => @@ -314,7 +347,7 @@ export class ImportExportService { } public exportWidgetsBundle(widgetsBundleId: string) { - this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe( + this.widgetService.exportWidgetsBundle(widgetsBundleId).subscribe( (widgetsBundle) => { this.dialog.open(ExportWidgetsBundleDialogComponent, { @@ -342,7 +375,7 @@ export class ImportExportService { } private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) { - this.widgetService.getBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe( + this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe( (widgetTypesDetails) => { const widgetsBundleItem: WidgetsBundleItem = { widgetsBundle: this.prepareExport(widgetsBundle), @@ -586,7 +619,7 @@ export class ImportExportService { } public exportDeviceProfile(deviceProfileId: string) { - this.deviceProfileService.getDeviceProfile(deviceProfileId).subscribe( + this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe( (deviceProfile) => { let name = deviceProfile.name; name = name.toLowerCase().replace(/\W/g, '_'); @@ -617,7 +650,7 @@ export class ImportExportService { } public exportAssetProfile(assetProfileId: string) { - this.assetProfileService.getAssetProfile(assetProfileId).subscribe( + this.assetProfileService.exportAssetProfile(assetProfileId).subscribe( (assetProfile) => { let name = assetProfile.name; name = name.toLowerCase().replace(/\W/g, '_'); @@ -818,6 +851,14 @@ export class ImportExportService { type: 'error'})); } + private validateImportedImage(image: ImageExportData): boolean { + return !(!isNotEmptyStr(image.data) + || !isNotEmptyStr(image.title) + || !isNotEmptyStr(image.fileName) + || !isNotEmptyStr(image.mediaType) + || !isNotEmptyStr(image.resourceKey)); + } + private validateImportedDashboard(dashboard: Dashboard): boolean { if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) { return false; @@ -929,30 +970,6 @@ export class ImportExportService { ); } - private editMissingAliases(widgets: Array, isSingleWidget: boolean, - customTitle: string, missingEntityAliases: EntityAliases): Observable { - return this.dialog.open(EntityAliasesDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - entityAliases: missingEntityAliases, - widgets, - customTitle, - isSingleWidget, - disableAdd: true - } - }).afterClosed().pipe( - map((updatedEntityAliases) => { - if (updatedEntityAliases) { - return updatedEntityAliases; - } else { - throw new Error('Unable to resolve missing entity aliases!'); - } - } - )); - } - private prepareAliasesInfo(aliasesInfo: AliasesInfo): AliasesInfo { const datasourceAliases = aliasesInfo.datasourceAliases; const targetDeviceAliases = aliasesInfo.targetDeviceAliases; diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html rename to ui-ngx/src/app/shared/import-export/table-columns-assignment.component.html diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.scss rename to ui-ngx/src/app/shared/import-export/table-columns-assignment.component.scss diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts rename to ui-ngx/src/app/shared/import-export/table-columns-assignment.component.ts index 096f7b3cfc..57f3607a2b 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts +++ b/ui-ngx/src/app/shared/import-export/table-columns-assignment.component.ts @@ -23,7 +23,7 @@ import { CsvColumnParam, ImportEntityColumnType, importEntityColumnTypeTranslations -} from '@home/components/import-export/import-export.models'; +} from '@shared/import-export/import-export.models'; import { BehaviorSubject, Observable } from 'rxjs'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 71d64f2df1..2cfe0531ff 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -26,6 +26,7 @@ import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/tab import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; import { UserId } from '@shared/models/id/user-id'; import { AlarmFilter } from '@shared/models/query/query.models'; +import { HasTenantId } from '@shared/models/entity.models'; export enum AlarmsMode { ALL, @@ -94,7 +95,7 @@ export const alarmSeverityColors = new Map( ] ); -export interface Alarm extends BaseData { +export interface Alarm extends BaseData, HasTenantId { tenantId: TenantId; customerId: CustomerId; assigneeId: UserId; diff --git a/ui-ngx/src/app/shared/models/asset.models.ts b/ui-ngx/src/app/shared/models/asset.models.ts index a1ab16fdc8..2e9b55f611 100644 --- a/ui-ngx/src/app/shared/models/asset.models.ts +++ b/ui-ngx/src/app/shared/models/asset.models.ts @@ -22,11 +22,11 @@ import { EntitySearchQuery } from '@shared/models/relation.models'; import { AssetProfileId } from '@shared/models/id/asset-profile-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { DashboardId } from '@shared/models/id/dashboard-id'; -import { EntityInfoData } from '@shared/models/entity.models'; +import { EntityInfoData, HasTenantId } from '@shared/models/entity.models'; export const TB_SERVICE_QUEUE = 'TbServiceQueue'; -export interface AssetProfile extends BaseData, ExportableEntity { +export interface AssetProfile extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; name: string; description?: string; @@ -44,7 +44,7 @@ export interface AssetProfileInfo extends EntityInfoData { defaultDashboardId?: DashboardId; } -export interface Asset extends BaseData, ExportableEntity { +export interface Asset extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; customerId?: CustomerId; name: string; diff --git a/ui-ngx/src/app/shared/models/customer.model.ts b/ui-ngx/src/app/shared/models/customer.model.ts index d26725814f..d770ba2420 100644 --- a/ui-ngx/src/app/shared/models/customer.model.ts +++ b/ui-ngx/src/app/shared/models/customer.model.ts @@ -18,8 +18,9 @@ import { CustomerId } from '@shared/models/id/customer-id'; import { ContactBased } from '@shared/models/contact-based.model'; import { TenantId } from './id/tenant-id'; import { ExportableEntity } from '@shared/models/base-data'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface Customer extends ContactBased, ExportableEntity { +export interface Customer extends ContactBased, HasTenantId, ExportableEntity { tenantId: TenantId; title: string; additionalInfo?: any; diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 8ccff354a7..14e8f6b1a8 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -23,8 +23,9 @@ import { Timewindow } from '@shared/models/time/time.models'; import { EntityAliases } from './alias.models'; import { Filters } from '@shared/models/query/query.models'; import { MatDialogRef } from '@angular/material/dialog'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface DashboardInfo extends BaseData, ExportableEntity { +export interface DashboardInfo extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; title?: string; image?: string; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 260fbee33a..fd277b52fb 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -22,7 +22,7 @@ import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id'; import { EntitySearchQuery } from '@shared/models/relation.models'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; -import { EntityInfoData } from '@shared/models/entity.models'; +import { EntityInfoData, HasTenantId } from '@shared/models/entity.models'; import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import * as _moment from 'moment'; @@ -584,7 +584,7 @@ export interface DeviceProfileData { provisionConfiguration?: DeviceProvisionConfiguration; } -export interface DeviceProfile extends BaseData, ExportableEntity { +export interface DeviceProfile extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; name: string; description?: string; @@ -603,7 +603,7 @@ export interface DeviceProfile extends BaseData, ExportableEnti defaultEdgeRuleChainId?: RuleChainId; } -export interface DeviceProfileInfo extends EntityInfoData { +export interface DeviceProfileInfo extends EntityInfoData, HasTenantId { tenantId?: TenantId; type: DeviceProfileType; transportType: DeviceTransportType; @@ -711,7 +711,7 @@ export interface DeviceData { transportConfiguration: DeviceTransportConfiguration; } -export interface Device extends BaseData, ExportableEntity { +export interface Device extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; customerId?: CustomerId; name: string; diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 7b5a6ddb3d..01789636b8 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -22,8 +22,9 @@ import { EntitySearchQuery } from '@shared/models/relation.models'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { BaseEventBody } from '@shared/models/event.models'; import { EventId } from '@shared/models/id/event-id'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface Edge extends BaseData { +export interface Edge extends BaseData, HasTenantId { tenantId?: TenantId; customerId?: CustomerId; name: string; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 5c9390f80f..adc750b52b 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -326,6 +326,8 @@ export const entityTypeTranslations = new Map([ [EntityType.ENTITY_VIEW, '/entities/entityViews'], [EntityType.TB_RESOURCE, '/resources/resources-library'], [EntityType.OTA_PACKAGE, '/features/otaUpdates'], - [EntityType.QUEUE, '/settings/queues'] + [EntityType.QUEUE, '/settings/queues'], + [EntityType.WIDGETS_BUNDLE, '/resources/widgets-library/widgets-bundles/details'], + [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'] ]); export interface EntitySubtype { diff --git a/ui-ngx/src/app/shared/models/entity-view.models.ts b/ui-ngx/src/app/shared/models/entity-view.models.ts index dd16914bc1..5c34e932c3 100644 --- a/ui-ngx/src/app/shared/models/entity-view.models.ts +++ b/ui-ngx/src/app/shared/models/entity-view.models.ts @@ -20,6 +20,7 @@ import { CustomerId } from '@shared/models/id/customer-id'; import { EntityViewId } from '@shared/models/id/entity-view-id'; import { EntityId } from '@shared/models/id/entity-id'; import { EntitySearchQuery } from '@shared/models/relation.models'; +import { HasTenantId } from '@shared/models/entity.models'; export interface AttributesEntityView { cs: Array; @@ -32,7 +33,7 @@ export interface TelemetryEntityView { attributes: AttributesEntityView; } -export interface EntityView extends BaseData, ExportableEntity { +export interface EntityView extends BaseData, HasTenantId, ExportableEntity { tenantId: TenantId; customerId: CustomerId; entityId: EntityId; diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 548b912b04..7e48eb2d43 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -19,6 +19,7 @@ import { AttributeData } from './telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import { DeviceCredentialMQTTBasic } from '@shared/models/device.models'; import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models'; +import { TenantId } from '@shared/models/id/tenant-id'; export interface EntityInfo { name?: string; @@ -162,3 +163,7 @@ export const entityFields: {[fieldName: string]: EntityField} = { value: 'label' } }; + +export interface HasTenantId { + tenantId?: TenantId; +} diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 7de754dadf..c47be6f312 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -27,6 +27,7 @@ import { AlarmSearchStatus, AlarmSeverity, AlarmStatus } from '@shared/models/al import { EntityType } from '@shared/models/entity-type.models'; import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models'; import { LimitedApi } from '@shared/models/limited-api.models'; +import { HasTenantId } from '@shared/models/entity.models'; export interface Notification { readonly id: NotificationId; @@ -109,7 +110,7 @@ export interface SlackConversation { type: string; } -export interface NotificationRule extends Omit, 'label'>, ExportableEntity { +export interface NotificationRule extends Omit, 'label'>, HasTenantId, ExportableEntity { tenantId: TenantId; enabled: boolean; templateId: NotificationTemplateId; @@ -241,7 +242,8 @@ export interface NonConfirmedNotificationEscalation { targets: Array; } -export interface NotificationTarget extends Omit, 'label'>, ExportableEntity { +export interface NotificationTarget extends Omit, 'label'>, HasTenantId, + ExportableEntity { tenantId: TenantId; configuration: NotificationTargetConfig; } @@ -295,7 +297,8 @@ export const NotificationTargetTypeTranslationMap = new Map, 'label'>, ExportableEntity { +export interface NotificationTemplate extends Omit, 'label'>, + HasTenantId, ExportableEntity { tenantId: TenantId; notificationType: NotificationType; configuration: NotificationTemplateConfig; diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index 3af7b9ae30..a53fff7b8e 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -18,6 +18,7 @@ import { BaseData } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { HasTenantId } from '@shared/models/entity.models'; export enum ChecksumAlgorithm { MD5 = 'MD5', @@ -85,7 +86,7 @@ export interface OtaPagesIds { softwareId?: OtaPackageId; } -export interface OtaPackageInfo extends BaseData { +export interface OtaPackageInfo extends BaseData, HasTenantId { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 07a57e68c2..22ebac12f1 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -17,6 +17,7 @@ import { BaseData } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { QueueId } from '@shared/models/id/queue-id'; +import { HasTenantId } from '@shared/models/entity.models'; export enum ServiceType { TB_CORE = 'TB_CORE', @@ -99,7 +100,7 @@ export const QueueProcessingStrategyTypesMap = new Map { +export interface QueueInfo extends BaseData, HasTenantId { generatedId?: string; name: string; packProcessingTimeout: number; diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 6621f10c42..fdaa89aee5 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -14,9 +14,11 @@ /// limitations under the License. /// -import { BaseData } from '@shared/models/base-data'; +import { BaseData, ExportableEntity, HasId } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { TbResourceId } from '@shared/models/id/tb-resource-id'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { HasTenantId } from '@shared/models/entity.models'; export enum ResourceType { LWM2M_MODEL = 'LWM2M_MODEL', @@ -52,20 +54,104 @@ export const ResourceTypeTranslationMap = new Map( ] ); -export interface ResourceInfo extends Omit, 'name' | 'label'> { +export interface TbResourceInfo extends Omit, 'name' | 'label'>, HasTenantId, ExportableEntity { tenantId?: TenantId; resourceKey?: string; title?: string; resourceType: ResourceType; + fileName: string; + descriptor?: D; } +export type ResourceInfo = TbResourceInfo; + export interface Resource extends ResourceInfo { data: string; - fileName: string; name?: string; } -export interface Resources extends ResourceInfo { - data: Array; - fileName: Array; +export interface ImageDescriptor { + mediaType: string; + width: number; + height: number; + size: number; + etag: string; + previewDescriptor: ImageDescriptor; +} + +export interface ImageResourceInfo extends TbResourceInfo { + link?: string; +} + +export interface ImageExportData { + mediaType: string; + fileName: string; + title: string; + resourceKey: string; + data: string; +} + +export type ImageResourceType = 'tenant' | 'system'; + +export type ImageReferences = {[entityType: string]: Array & HasTenantId>}; + +export interface ImageResourceInfoWithReferences extends ImageResourceInfo { + references: ImageReferences; +} + +export interface ImageDeleteResult { + image: ImageResourceInfo; + success: boolean; + imageIsReferencedError?: boolean; + error?: any; + references?: ImageReferences; } + +export const toImageDeleteResult = (image: ImageResourceInfo, e?: any): ImageDeleteResult => { + if (!e) { + return {image, success: true}; + } else { + const result: ImageDeleteResult = {image, success: false, error: e}; + if (e?.status === 400 && e?.error?.success === false && e?.error?.references) { + const references: ImageReferences = e?.error?.references; + result.imageIsReferencedError = true; + result.references = references; + } + return result; + } +}; + +export const imageResourceType = (imageInfo: ImageResourceInfo): ImageResourceType => + (!imageInfo.tenantId || imageInfo.tenantId?.id === NULL_UUID) ? 'system' : 'tenant'; + +export const TB_IMAGE_PREFIX = 'tb-image;'; + +export const IMAGES_URL_REGEXP = /\/api\/images\/(tenant|system)\/(.*)/; +export const IMAGES_URL_PREFIX = '/api/images'; + +export const IMAGE_BASE64_URL_PREFIX = 'data:image/'; + +export const removeTbImagePrefix = (url: string): string => url ? url.replace(TB_IMAGE_PREFIX, '') : url; + +export const removeTbImagePrefixFromUrls = (urls: string[]): string[] => urls ? urls.map(url => removeTbImagePrefix(url)) : []; + +export const prependTbImagePrefix = (url: string): string => { + if (url && !url.startsWith(TB_IMAGE_PREFIX)) { + url = TB_IMAGE_PREFIX + url; + } + return url; +}; + +export const prependTbImagePrefixToUrls = (urls: string[]): string[] => urls ? urls.map(url => prependTbImagePrefix(url)) : []; + + +export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_REGEXP.test(url); + +export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => { + const res = url.match(IMAGES_URL_REGEXP); + return {type: res[1] as ImageResourceType, key: res[2]}; +}; + +export const isBase64DataImageUrl = (url: string): boolean => url && url.startsWith(IMAGE_BASE64_URL_PREFIX); + +export const NO_IMAGE_DATA_URI = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 85dcbf3a6e..96fa1034c2 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -20,8 +20,9 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface RuleChain extends BaseData, ExportableEntity { +export interface RuleChain extends BaseData, HasTenantId, ExportableEntity { tenantId: TenantId; name: string; firstRuleNodeId: RuleNodeId; diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 73b3d9de02..6cde443f62 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -33,6 +33,7 @@ export interface DefaultTenantProfileConfiguration { maxRuleChains: number; maxResourcesInBytes: number; maxOtaPackagesInBytes: number; + maxResourceSize: number; transportTenantMsgRateLimit?: string; transportTenantTelemetryMsgRateLimit?: string; @@ -101,6 +102,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxRuleChains: 0, maxResourcesInBytes: 0, maxOtaPackagesInBytes: 0, + maxResourceSize: 0, maxTransportMessages: 0, maxTransportDataPoints: 0, maxREExecutions: 0, diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts index 02a8c657f9..21c6a7506a 100644 --- a/ui-ngx/src/app/shared/models/user.model.ts +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -19,8 +19,9 @@ import { UserId } from './id/user-id'; import { CustomerId } from './id/customer-id'; import { Authority } from './authority.enum'; import { TenantId } from './id/tenant-id'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface User extends BaseData { +export interface User extends BaseData, HasTenantId { tenantId: TenantId; customerId: CustomerId; email: string; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 5228af05b0..b682b79600 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -32,6 +32,7 @@ export const exportableEntityTypes: Array = [ EntityType.RULE_CHAIN, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, + EntityType.TB_RESOURCE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE @@ -40,7 +41,8 @@ export const exportableEntityTypes: Array = [ export const entityTypesWithoutRelatedData: Set = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, - EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_RULE, + EntityType.TB_RESOURCE ]); export interface VersionCreateConfig { diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index 27b32621aa..ec28b3ebdf 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { isDefinedAndNotNull, isNumber, isNumeric, parseFunction } from '@core/utils'; +import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, parseFunction } from '@core/utils'; import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models'; import { Injector } from '@angular/core'; import { DatePipe } from '@angular/common'; @@ -22,6 +22,10 @@ import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { TranslateService } from '@ngx-translate/core'; import { AlarmFilterConfig } from '@shared/models/query/query.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models'; +import { Observable, of } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { map } from 'rxjs/operators'; +import { DomSanitizer } from '@angular/platform-browser'; export type ComponentStyle = {[klass: string]: any}; @@ -85,6 +89,79 @@ export interface ColorRange { color: string; } +export const colorRangeIncludes = (range: ColorRange, toCheck: ColorRange): boolean => { + if (isNumber(range.from) && isNumber(range.to)) { + if (isNumber(toCheck.from) && isNumber(toCheck.to)) { + return toCheck.from >= range.from && toCheck.to < range.to; + } else { + return false; + } + } else if (isNumber(range.from)) { + if (isNumber(toCheck.from)) { + return toCheck.from >= range.from; + } else { + return false; + } + } else if (isNumber(range.to)) { + if (isNumber(toCheck.to)) { + return toCheck.to < range.to; + } else { + return false; + } + } else { + return false; + } +}; + +export const filterIncludingColorRanges = (ranges: Array): Array => { + const result = [...ranges]; + let includes = true; + while (includes) { + let index = -1; + for (let i = 0; i < result.length; i++) { + const range = result[i]; + if (result.some((value, i1) => i1 !== i && colorRangeIncludes(value, range))) { + index = i; + break; + } + } + if (index > -1) { + result.splice(index, 1); + } else { + includes = false; + } + } + return result; +}; + +export const sortedColorRange = (ranges: Array): Array => ranges ? [...ranges].sort( + (a, b) => { + if (isNumber(a.from) && isNumber(a.to) && isNumber(b.from) && isNumber(b.to)) { + if (b.from >= a.from && b.to < a.to) { + return 1; + } else if (a.from >= b.from && a.to < b.to) { + return -1; + } else { + return a.from - b.from; + } + } else if (isNumber(a.from) && isNumber(b.from)) { + return a.from - b.from; + } else if (isNumber(a.to) && isNumber(b.to)) { + return a.to - b.to; + } else if (isNumber(a.from) && isUndefinedOrNull(b.from)) { + return 1; + } else if (isUndefinedOrNull(a.from) && isNumber(b.from)) { + return -1; + } else if (isNumber(a.to) && isUndefinedOrNull(b.to)) { + return 1; + } else if (isUndefinedOrNull(a.to) && isNumber(b.to)) { + return -1; + } else { + return 0; + } + } + ) : []; + export interface ColorSettings { type: ColorType; color: string; @@ -208,7 +285,7 @@ class RangeColorProcessor extends ColorProcessor { return this.settings.color; } - private static constantRange(range: ColorRange): boolean { + public static constantRange(range: ColorRange): boolean { return isNumber(range.from) && isNumber(range.to) && range.from === range.to; } } @@ -343,14 +420,12 @@ export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor { export enum BackgroundType { image = 'image', - imageUrl = 'imageUrl', color = 'color' } export const backgroundTypeTranslations = new Map( [ [BackgroundType.image, 'widgets.background.background-type-image'], - [BackgroundType.imageUrl, 'widgets.background.background-type-image-url'], [BackgroundType.color, 'widgets.background.background-type-color'] ] ); @@ -363,7 +438,6 @@ export interface OverlaySettings { export interface BackgroundSettings { type: BackgroundType; - imageBase64?: string; imageUrl?: string; color?: string; overlay: OverlaySettings; @@ -434,19 +508,36 @@ export const isFontSet = (font: Font): boolean => (!!font && !!font.style && !!f export const isFontPartiallySet = (font: Font): boolean => (!!font && (!!font.style || !!font.weight || !!font.size || !!font.family)); -export const backgroundStyle = (background: BackgroundSettings): ComponentStyle => { +export const validateAndUpdateBackgroundSettings = (background: BackgroundSettings): BackgroundSettings => { + if (background) { + if (background.type === BackgroundType.image && (background as any).imageBase64) { + background.imageUrl = (background as any).imageBase64; + } + if (background.type === 'imageUrl' as any) { + background.type = BackgroundType.image; + } + delete (background as any).imageBase64; + } + return background; +}; + +export const backgroundStyle = (background: BackgroundSettings, imagePipe: ImagePipe, + sanitizer: DomSanitizer, preview = false): Observable => { + background = validateAndUpdateBackgroundSettings(background); if (background.type === BackgroundType.color) { - return { + return of({ background: background.color - }; + }); } else { - const imageUrl = background.type === BackgroundType.image ? background.imageBase64 : background.imageUrl; + const imageUrl = background.imageUrl; if (imageUrl) { - return { - background: `url(${imageUrl}) no-repeat 50% 50% / cover` - }; + return imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true, preview}).pipe( + map((transformedUrl) => ({ + background: sanitizer.bypassSecurityTrustStyle(`url(${transformedUrl}) no-repeat 50% 50% / cover`) + })) + ); } else { - return {}; + return of({}); } } }; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 0e676138ce..e196dd7cdb 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -42,6 +42,7 @@ import { isNotEmptyStr } from '@core/utils'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { HasTenantId } from '@shared/models/entity.models'; export enum widgetType { timeseries = 'timeseries', @@ -193,7 +194,7 @@ export interface WidgetControllerDescriptor { actionSources?: {[actionSourceId: string]: WidgetActionSource}; } -export interface BaseWidgetType extends BaseData { +export interface BaseWidgetType extends BaseData, HasTenantId { tenantId: TenantId; fqn: string; name: string; @@ -269,6 +270,8 @@ export enum LegendPosition { right = 'right' } +export const legendPositions = Object.keys(LegendPosition) as LegendPosition[]; + export const legendPositionTranslationMap = new Map( [ [ LegendPosition.top, 'position.top' ], diff --git a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts index 476c6ee029..a6418157b4 100644 --- a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts +++ b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts @@ -17,8 +17,9 @@ import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { WidgetsBundleId } from '@shared/models/id/widgets-bundle-id'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface WidgetsBundle extends BaseData, ExportableEntity { +export interface WidgetsBundle extends BaseData, HasTenantId, ExportableEntity { tenantId: TenantId; alias: string; title: string; diff --git a/ui-ngx/src/app/shared/pipe/image.pipe.ts b/ui-ngx/src/app/shared/pipe/image.pipe.ts new file mode 100644 index 0000000000..43acf04c77 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/image.pipe.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { ImageService } from '@core/http/image.service'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { AsyncSubject, BehaviorSubject, Observable } from 'rxjs'; +import { isDefinedAndNotNull } from '@core/utils'; +import { NO_IMAGE_DATA_URI } from '@shared/models/resource.models'; + +const LOADING_IMAGE_DATA_URI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRG' + + 'LTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAw' + + 'IDIwIDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPgo='; + +export interface UrlHolder { + url?: string; +} + +@Pipe({ + name: 'image' +}) +export class ImagePipe implements PipeTransform { + + constructor(private imageService: ImageService, + private sanitizer: DomSanitizer) { } + + transform(urlData: string | UrlHolder, args?: any): Observable { + const ignoreLoadingImage = !!args?.ignoreLoadingImage; + const asString = !!args?.asString; + const emptyUrl = args?.emptyUrl || NO_IMAGE_DATA_URI; + const image$ = ignoreLoadingImage + ? new AsyncSubject() + : new BehaviorSubject(LOADING_IMAGE_DATA_URI); + const url = (typeof urlData === 'string') ? urlData : urlData?.url; + if (isDefinedAndNotNull(url)) { + const preview = !!args?.preview; + this.imageService.resolveImageUrl(url, preview, asString, emptyUrl).subscribe((imageUrl) => { + Promise.resolve().then(() => { + image$.next(imageUrl); + image$.complete(); + }); + }); + } else { + Promise.resolve().then(() => { + image$.next(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl)); + image$.complete(); + }); + } + return image$.asObservable(); + } + +} diff --git a/ui-ngx/src/app/shared/pipe/public-api.ts b/ui-ngx/src/app/shared/pipe/public-api.ts index 9ba3be3610..f03d669345 100644 --- a/ui-ngx/src/app/shared/pipe/public-api.ts +++ b/ui-ngx/src/app/shared/pipe/public-api.ts @@ -23,3 +23,4 @@ export * from './nospace.pipe'; export * from './truncate.pipe'; export * from './file-size.pipe'; export * from './selectable-columns.pipe'; +export * from './image.pipe'; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 042e453fe1..ebff0d0a63 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -200,6 +200,20 @@ import { ColorPickerPanelComponent } from '@shared/components/color-picker/color import { TbIconComponent } from '@shared/components/icon.component'; import { HintTooltipIconComponent } from '@shared/components/hint-tooltip-icon.component'; import { StringAutocompleteComponent } from '@shared/components/string-autocomplete.component'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { ImportDialogComponent } from '@shared/import-export/import-dialog.component'; +import { ImportDialogCsvComponent } from '@shared/import-export/import-dialog-csv.component'; +import { ExportWidgetsBundleDialogComponent } from '@shared/import-export/export-widgets-bundle-dialog.component'; +import { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component'; +import { ScrollGridComponent } from '@shared/components/grid/scroll-grid.component'; +import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; +import { UploadImageDialogComponent } from '@shared/components/image/upload-image-dialog.component'; +import { ImageDialogComponent } from '@shared/components/image/image-dialog.component'; +import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; +import { ImagesInUseDialogComponent } from '@shared/components/image/images-in-use-dialog.component'; +import { GalleryImageInputComponent } from '@shared/components/image/gallery-image-input.component'; +import { MultipleGalleryImageInputComponent } from '@shared/components/image/multiple-gallery-image-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -217,6 +231,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) DateAgoPipe, SafePipe, ShortNumberPipe, + ImagePipe, { provide: FlowInjectionToken, useValue: Flow @@ -234,6 +249,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { provide: SHARED_MODULE_TOKEN, useValue: SharedModule }, { provide: MatPaginatorIntl, useClass: CustomPaginatorIntl }, TbPopoverService, + ImportExportService, { provide: MAT_SELECT_CONFIG, useValue: { @@ -340,6 +356,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TbJsonPipe, FileSizePipe, DateAgoPipe, + ImagePipe, SafePipe, ShortNumberPipe, SelectableColumnsPipe, @@ -379,7 +396,19 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MaterialIconsComponent, RuleChainSelectComponent, TbIconComponent, - HintTooltipIconComponent + HintTooltipIconComponent, + ImportDialogComponent, + ImportDialogCsvComponent, + ExportWidgetsBundleDialogComponent, + TableColumnsAssignmentComponent, + ScrollGridComponent, + ImageGalleryComponent, + UploadImageDialogComponent, + ImageDialogComponent, + ImageReferencesComponent, + ImagesInUseDialogComponent, + GalleryImageInputComponent, + MultipleGalleryImageInputComponent ], imports: [ CommonModule, @@ -576,6 +605,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyboardShortcutPipe, FileSizePipe, DateAgoPipe, + ImagePipe, SafePipe, ShortNumberPipe, SelectableColumnsPipe, @@ -615,7 +645,19 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MaterialIconsComponent, RuleChainSelectComponent, TbIconComponent, - HintTooltipIconComponent + HintTooltipIconComponent, + ImportDialogComponent, + ImportDialogCsvComponent, + ExportWidgetsBundleDialogComponent, + TableColumnsAssignmentComponent, + ScrollGridComponent, + ImageGalleryComponent, + UploadImageDialogComponent, + ImageDialogComponent, + ImageReferencesComponent, + ImagesInUseDialogComponent, + GalleryImageInputComponent, + MultipleGalleryImageInputComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ef660731bf..d49bf8dcac 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -77,7 +77,10 @@ "show-more": "Show more", "dont-show-again": "Do not show again", "see-documentation": "See documentation", - "clear": "Clear" + "clear": "Clear", + "upload": "Upload", + "delete-anyway": "Delete anyway", + "delete-selected": "Delete selected" }, "aggregation": { "aggregation": "Aggregation", @@ -2198,6 +2201,8 @@ "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "type-tb-resource": "Resource", + "type-tb-resources": "Resources", + "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "type-ota-package": "OTA package", "type-rpc": "RPC", "type-queue": "Queue", @@ -2940,6 +2945,53 @@ "browse-file": "Browse file", "browse-files": "Browse files" }, + "image": { + "gallery": "Image gallery", + "search": "Search image", + "selected-images": "{ count, plural, =1 {1 image} other {# images} } selected", + "created-time": "Created time", + "name": "Name", + "name-required": "Name is required.", + "resolution": "Resolution", + "size": "Size", + "system": "System", + "download-image": "Download image", + "export-image": "Export image", + "import-image": "Import image", + "upload-image": "Upload image", + "edit-image": "Edit image", + "image-details": "Image details", + "no-images": "No images found", + "delete-image": "Delete image", + "delete-image-title": "Are you sure you want to delete image '{{imageTitle}}'?", + "delete-image-text": "Be careful, after the confirmation image will become unrecoverable.", + "delete-images-title": "Are you sure you want to delete { count, plural, =1 {1 image} other {# images} }?", + "delete-images-text": "Be careful, after the confirmation all selected images will be removed and all related data will become unrecoverable.", + "list-mode": "List view", + "grid-mode": "Grid view", + "image-preview": "Image preview", + "update-image": "Update image", + "export-failed-error": "Unable to export image: {{error}}", + "image-json-file": "Image JSON file", + "invalid-image-json-file-error": "Unable to import image from JSON: Invalid image JSON data structure.", + "image-is-in-use": "Image is used by other entities", + "images-are-in-use": "Images are used by other entities", + "image-is-in-use-text": "The image '{{title}}' was not deleted because it is used by the following entities:", + "images-are-in-use-text": "Not all images have been deleted because they are used by other entities.
You can view referenced entities by clicking the References button in the corresponding image row.
If you still want to delete these images, select them in the table below and click the Delete selected button.", + "delete-image-in-use-text": "If you still want to delete the image, click the Delete anyway button.", + "system-entities": "System entities:", + "entities": "entities:", + "references": "References", + "include-system-images": "Include system images", + "clear-image": "Clear image", + "no-image": "No image", + "no-image-selected": "No image selected", + "browse-from-gallery": "Browse from gallery", + "set-link": "Set link", + "image-link": "Image link", + "link": "Link", + "copy-image-link": "Copy image link" + }, "image-input": { "drop-images-or": "Drag and drop an images or", "drag-and-drop": "Drag & Drop", @@ -3966,7 +4018,7 @@ "rule-engine": "Rule Engine", "time-to-live": "Time-to-live", "alarms-and-notifications": "Alarms and notifications", - "ota-files-in-bytes": "OTA files in bytes", + "ota-files-in-bytes": "Files", "ws-title": "WS", "unlimited": "(0 - unlimited)", "maximum-devices": "Devices maximum number", @@ -3989,10 +4041,13 @@ "maximum-rule-chains-range": "Rule chains maximum number can't be negative", "maximum-resources-sum-data-size": "Resource files sum size", "maximum-resources-sum-data-size-required": "Resource files sum size is required.", - "maximum-resources-sum-data-size-range": "Resource files sum size can`t be negative", + "maximum-resources-sum-data-size-range": "Resource files sum size can't be negative", + "maximum-resource-size": "Resource maximum size", + "maximum-resource-size-required": "Resource maximum size is required", + "maximum-resource-size-range": "Resource maximum size can't be negative", "maximum-ota-packages-sum-data-size": "OTA package files sum size", "maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.", - "maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative", + "maximum-ota-package-sum-data-size-range": "OTA package files sum size can't be negative", "rest-requests-for-tenant": "REST requests for tenant", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", @@ -5068,9 +5123,8 @@ "background": { "background": "Background", "background-settings": "Background settings", - "background-type-image": "Upload image", - "background-type-image-url": "Image URL", - "background-type-color": "Solid color", + "background-type-image": "Image", + "background-type-color": "Color", "image-url": "Image URL", "overlay": "Overlay", "enable-overlay": "Enable overlay", @@ -5834,6 +5888,20 @@ "no-columns-found": "No columns found", "no-columns-matching": "'{{column}}' not found." }, + "range-chart": { + "chart": "Chart", + "data-zoom": "Data zoom", + "range-colors": "Range colors", + "out-of-range-color": "Out of range color", + "fill-area": "Fill area", + "legend-label": "Label", + "tooltip": "Tooltip", + "tooltip-value": "Value", + "tooltip-date": "Date", + "tooltip-background-color": "Background color", + "tooltip-background-blur": "Background blur", + "range-chart-card-style": "Range chart card style" + }, "rpc": { "value-settings": "Value settings", "initial-value": "Initial value", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 3b29fe986c..3e6ea6a2cc 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -523,6 +523,7 @@ right: 0; background: $tb-primary-color; opacity: 0.04; + pointer-events: none; } }