Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/entity-alarm-rules

pull/14291/head
Viacheslav Klimov 11 months ago
parent
commit
d2eaa1718c
  1. 4
      application/src/main/data/json/system/widget_types/attributes_card.json
  2. 3
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  3. 20
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  4. 2
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  5. 9
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java
  6. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java
  7. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
  8. 8
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  9. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  10. 1
      common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java
  11. 12
      common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java
  12. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  13. 36
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java
  14. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  15. 7
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
  16. 10
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  17. 8
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  18. 11
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  19. 8
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  20. 8
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  21. 9
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  22. 11
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  23. 26
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  24. 16
      dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java
  25. 10
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  26. 20
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  27. 30
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  28. 18
      dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java
  29. 15
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java
  30. 14
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java
  31. 13
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java
  32. 14
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java
  33. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  34. 14
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  35. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
  36. 9
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java
  37. 8
      dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
  38. 36
      dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java
  39. 8
      dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java
  40. 34
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  41. 13
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  42. 35
      dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java
  43. 38
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  44. 9
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
  45. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java
  46. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java
  47. 30
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
  48. 10
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  49. 11
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  50. 1
      dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
  51. 15
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  52. 12
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  53. 48
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
  54. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java
  55. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
  56. 26
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  57. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  58. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  59. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  60. 50
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
  61. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java
  62. 111
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  63. 90
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
  64. 154
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java
  65. 2
      ui-ngx/src/app/core/http/admin.service.ts
  66. 38
      ui-ngx/src/app/core/http/git-hub.service.ts
  67. 10
      ui-ngx/src/app/core/utils.ts
  68. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  69. 3
      ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts
  70. 1
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  71. 14
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  72. 26
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.html
  73. 35
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.scss
  74. 34
      ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts
  75. 6
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html
  76. 1
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss
  77. 2
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html
  78. 9
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  79. 9
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  80. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.html
  81. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html
  82. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts
  83. 1
      ui-ngx/src/app/modules/home/home.component.html
  84. 2
      ui-ngx/src/app/modules/home/home.module.ts
  85. 3
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html
  86. 4
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts
  87. 8
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  88. 2
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  89. 4
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  90. 2
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html
  91. 3
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts
  92. 2
      ui-ngx/src/app/modules/home/pages/user/user.component.html
  93. 6
      ui-ngx/src/app/modules/home/pages/user/user.component.ts
  94. 2
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  95. 7
      ui-ngx/src/app/modules/login/pages/login/login.component.ts
  96. 2
      ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html
  97. 3
      ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts
  98. 2
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html
  99. 12
      ui-ngx/src/app/shared/components/file-input.component.html
  100. 2
      ui-ngx/src/app/shared/components/phone-input.component.html

4
application/src/main/data/json/system/widget_types/attributes_card.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 \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"<div id='\" + datasourceId +\n \"' class='tbDatasource-container'></div>\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"<div class='tbDatasource-title'>\" +\n tbDatasource.name + \"</div>\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"<table id='\" + tableId +\n \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"<tr><td id='\" + labelCellId + \"'>\" + dataKey.label +\n \"</td><td id='\" + cellId +\n \"'></td></tr>\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n\n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n for (var i = 0; i < self.ctx.datasources\n .length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"<div id='\" + datasourceId +\n \"' class='tbDatasource-container'></div>\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"<div class='tbDatasource-title'>\" +\n tbDatasource.name + \"</div>\"\n );\n\n var datasourceTitleCell = $(\n '.tbDatasource-title',\n datasourceContainer);\n self.ctx.datasourceTitleCells.push(\n datasourceTitleCell);\n\n var tableId = 'table' + i;\n datasourceContainer.append(\n \"<table id='\" + tableId +\n \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n );\n var table = $('#' + tableId, self.ctx\n .$container);\n\n for (var a = 0; a < tbDatasource.dataKeys\n .length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"<tr><td id='\" + labelCellId +\n \"'>\" + dataKey.label +\n \"</td><td id='\" + cellId +\n \"'></td></tr>\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n }\n\n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells\n .length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data\n .length > 0) {\n var tvPair = cellData.data[cellData.data\n .length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals ||\n cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey\n .decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(\n value, decimals, units, true);\n } else {\n txtValue = self.ctx.utilsService\n .customTranslation(value);\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n\n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height / 8;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width / 12;\n }\n datasourceTitleFontSize = Math.min(\n datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells\n .length; i++) {\n self.ctx.datasourceTitleCells[i].css(\n 'font-size', datasourceTitleFontSize +\n 'px');\n }\n var valueFontSize = self.ctx.height / 9;\n var labelFontSize = self.ctx.height / 9;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width / 15;\n labelFontSize = self.ctx.width / 15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size',\n valueFontSize + 'px');\n self.ctx.valueCells[i].css('height',\n valueFontSize * 2.5 + 'px');\n self.ctx.valueCells[i].css('padding', '0px ' +\n valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size',\n labelFontSize + 'px');\n self.ctx.labelCells[i].css('height',\n labelFontSize * 2.5 + 'px');\n self.ctx.labelCells[i].css('padding', '0px ' +\n labelFontSize + 'px');\n }\n}\n\nself.onDestroy = function() {}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\",\"decimals\":null}"
@ -29,4 +29,4 @@
"public": true
}
]
}
}

3
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -266,6 +266,9 @@ public class UserController extends BaseController {
if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) {
throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED);
}
if (user.getAuthority() == Authority.TENANT_ADMIN && userService.countTenantAdmins(user.getTenantId()) == 1) {
throw new ThingsboardException("At least one tenant administrator must remain!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
tbUserService.delete(getTenantId(), getCurrentUser().getCustomerId(), user, getCurrentUser());
}

20
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -284,6 +284,26 @@ public class UserControllerTest extends AbstractControllerTest {
ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testShouldNotDeleteLastTenantAdmin() throws Exception {
loginSysAdmin();
User tenantAdmin2 = new User();
tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin2.setTenantId(tenantId);
tenantAdmin2.setEmail("tenant2@thingsboard.io");
tenantAdmin2 = doPost("/api/user", tenantAdmin2, User.class);
// delete second tenant admin - ok
doDelete("/api/user/" + tenantAdmin2.getId().getId().toString())
.andExpect(status().isOk());
// delete last tenant admin - forbidden
doDelete("/api/user/" + tenantAdminUser.getId().getId().toString())
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("At least one tenant administrator must remain!")));
}
@Test
public void testSaveUserWithInvalidEmail() throws Exception {
loginSysAdmin();

2
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -98,7 +98,7 @@ public class JobManagerTest extends AbstractControllerTest {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getSuccessfulCount()).isBetween(0, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
});
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {

9
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java

@ -27,17 +27,16 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest {
/*
* Some tests are overridden because they are based on
* tenant partitioning strategy (subsequent tasks processing within a tenant)
* */
* Some tests are overridden because they are based on
* tenant partitioning strategy (subsequent tasks processing within a tenant)
* */
@Override
public void testCancelJob_simulateTaskProcessorRestart() throws Exception {
public void testCancelJob_simulateTaskProcessorRestart() {
}
@Override
public void testSubmitJob_generalError() {
}
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -26,6 +27,8 @@ public interface EntityDaoService {
Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId);
FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId);
default long countByTenantId(TenantId tenantId) {
throw new IllegalArgumentException("Not implemented for " + getEntityType());
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -38,6 +39,8 @@ public interface EntityService {
Optional<CustomerId> fetchEntityCustomerId(TenantId tenantId, EntityId entityId);
FluentFuture<Optional<CustomerId>> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId);
Optional<HasId<?>> fetchEntity(TenantId tenantId, EntityId entityId);
Map<EntityId, EntityInfo> fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set<EntityId> entityIds);
@ -47,4 +50,5 @@ public interface EntityService {
long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query);
PageData<EntityData> findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query);
}

8
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -32,9 +32,6 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
/**
* Created by Victor Basanets on 8/27/2017.
*/
public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView);
@ -55,6 +52,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache);
ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId);
EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<EntityView> findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name);
@ -77,8 +76,6 @@ public interface EntityViewService extends EntityDaoService {
ListenableFuture<List<EntityView>> findEntityViewsByQuery(TenantId tenantId, EntityViewSearchQuery query);
ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId);
List<EntityView> findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
@ -98,4 +95,5 @@ public interface EntityViewService extends EntityDaoService {
PageData<EntityView> findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink);
PageData<EntityView> findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -111,8 +111,9 @@ public interface UserService extends EntityDaoService {
void removeMobileSession(TenantId tenantId, String mobileToken);
int countTenantAdmins(TenantId tenantId);
PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink);
boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java

@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.id.CustomerId;
public interface HasCustomerId {
CustomerId getCustomerId();
}

12
common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java

@ -55,11 +55,17 @@ public abstract class JobResult implements Serializable {
public void processTaskResult(TaskResult taskResult) {
if (taskResult.isSuccess()) {
successfulCount++;
if (totalCount == null || successfulCount < totalCount) {
successfulCount++;
}
} else if (taskResult.isDiscarded()) {
discardedCount++;
if (totalCount == null || discardedCount < totalCount) {
discardedCount++;
}
} else {
failedCount++;
if (totalCount == null || failedCount < totalCount) {
failedCount++;
}
if (results.size() < 100) { // preserving only first 100 errors, not reprocessing if there are more failures
results.add(taskResult);
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
@ -196,14 +195,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
root = new CoapResource("");
coapServer.add(root);
}
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE, serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE)));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE,serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE)));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE));
}
return leshanServer;
}
private void setServerWithCredentials(LeshanServerBuilder builder) {
// private void setServerWithCredentials(LeshanServerBuilder builder) {
if (this.config.getSslCredentials() != null) {
SslCredentials sslCredentials = this.config.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());

36
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java

@ -34,21 +34,15 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SOFTWARE_UPDATE_COAP_RESOURCE;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.calculateSzx;
@Slf4j
public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
private final ConcurrentMap<String, ObserveRelation> tokenToObserveRelationMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, AtomicInteger> tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>();
private final OtaPackageDataCache otaPackageDataCache;
private final int chunkSize;
private final int maxResourceBodySize;
public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name, int chunkSize, int maxResourceBodySize) {
public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name) {
super(name);
this.otaPackageDataCache = otaPackageDataCache;
this.chunkSize = chunkSize;
this.maxResourceBodySize = maxResourceBodySize;
this.setObservable(true); // enable observing
this.addObserver(new CoapResourceObserver());
}
@ -141,29 +135,23 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 1
);
UUID currentId = UUID.fromString(idStr);
log.info("Start Read ota data (path): [{}]", exchange.getRequestOptions().getUriPath().toString());
Response response = new Response(CoAP.ResponseCode.CONTENT);
byte[] otaData = this.getOtaData(currentId);
if (otaData != null && otaData.length > 0) {
if (otaData.length <= this.maxResourceBodySize) {
log.info("Read ota data (length): [{}]", otaData.length);
response.setPayload(otaData);
int chunkSize = calculateSzx(this.chunkSize);
if (exchange.getRequestOptions().hasBlock2()) {
chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
} else if (exchange.getRequestOptions().hasBlock1()) {
chunkSize = exchange.getRequestOptions().getBlock1().getSzx();
}
log.info("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), otaData.length, chunkSize, false);
boolean lastFlag = otaData.length <= this.chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
response.setType(CoAP.Type.CON);
exchange.respond(response);
log.debug("Read ota data (length): [{}]", otaData.length);
response.setPayload(otaData);
if (exchange.getRequestOptions().getBlock2() != null) {
int szx = exchange.getRequestOptions().getBlock2().getSzx();
int chunkSize = exchange.getRequestOptions().getBlock2().getSize();
boolean lastFlag = otaData.length <= chunkSize;
response.getOptions().setBlock2(szx, lastFlag, 0);
log.trace("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], szx [{}], moreFlag [{}]", currentId, otaData.length, chunkSize, szx, lastFlag);
} else {
log.info("Ota package size: [{}] is larger than server's MAX_RESOURCE_BODY_SIZE [{}]", otaData.length, this.maxResourceBodySize);
log.trace("With block1 Send currentId: [{}], length: [{}], ", currentId, otaData.length);
}
exchange.respond(response);
} else {
log.info("Ota packaged currentId: [{}] is not found.", currentId.toString());
log.trace("Ota packaged currentId: [{}] is not found.", currentId);
}
}

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -121,10 +121,6 @@ public class LwM2MTransportUtil {
}
}
public static List<LwM2MBootstrapServerCredential> getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) {
return toLwM2MClientProfile(deviceProfile).getBootstrap();
}
public static String fromVersionedIdToObjectId(String pathIdVer) {
try {
if (pathIdVer == null) {
@ -399,13 +395,6 @@ public class LwM2MTransportUtil {
}
}
public static int calculateSzx(int size) {
if (size < 16 || size > 1024 || (size & (size - 1)) != 0) {
throw new IllegalArgumentException("Size must be a power of 2 between 16 and 1024.");
}
return (int) (Math.log(size / 16) / Math.log(2));
}
public static ConcurrentHashMap<Integer, String[]> groupByObjectIdVersionedIds(Set<String> targetIds) {
return targetIds.stream()
.collect(Collectors.groupingBy(

7
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java

@ -39,6 +39,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@ -135,6 +136,12 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
.map(model -> model); // necessary to cast to HasId<?>
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return findAiModelByTenantIdAndIdAsync(tenantId, new AiModelId(entityId.getId()))
.transform(modelOpt -> modelOpt.map(model -> model), directExecutor()); // necessary to cast to HasId<?>
}
@Override
public long countByTenantId(TenantId tenantId) {
return aiModelDao.countByTenantId(tenantId);

10
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
@ -81,6 +80,7 @@ import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -97,8 +97,8 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
private final AlarmDao alarmDao;
private final EntityService entityService;
@TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(AlarmTypesCacheEvictEvent event) {
TenantId tenantId = event.getTenantId();
cache.evict(tenantId);
@ -460,6 +460,12 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return Optional.ofNullable(findAlarmById(tenantId, new AlarmId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findAlarmByIdAsync(tenantId, new AlarmId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ALARM;

8
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -49,6 +50,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("AssetProfileDaoService")
@ -323,6 +325,12 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
return Optional.ofNullable(findAssetProfileById(tenantId, new AssetProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(assetProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;

11
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -64,6 +64,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -95,8 +96,8 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Autowired
private JpaExecutorService executor;
@TransactionalEventListener(classes = AssetCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(AssetCacheEvictEvent event) {
List<AssetCacheKey> keys = new ArrayList<>(2);
keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName()));
@ -531,6 +532,12 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return Optional.ofNullable(findAssetById(tenantId, new AssetId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findAssetByIdAsync(tenantId, new AssetId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return assetDao.countByTenantId(tenantId);

8
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -42,6 +43,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -245,6 +247,12 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return Optional.ofNullable(findById(tenantId, new CalculatedFieldId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(calculatedFieldDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;

8
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.customer;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -57,6 +58,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("CustomerDaoService")
@ -302,6 +304,12 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
return Optional.ofNullable(findCustomerById(tenantId, new CustomerId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findCustomerByIdAsync(tenantId, new CustomerId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return customerDao.countByTenantId(tenantId);

9
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -61,6 +62,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("DashboardDaoService")
@ -70,6 +72,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
public static final String INCORRECT_DASHBOARD_ID = "Incorrect dashboardId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Autowired
private DashboardDao dashboardDao;
@ -424,6 +427,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return Optional.ofNullable(findDashboardById(tenantId, new DashboardId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findDashboardByIdAsync(tenantId, new DashboardId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return dashboardDao.countByTenantId(tenantId);

11
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -64,6 +65,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@ -369,6 +371,12 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
return Optional.ofNullable(findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(deviceProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE;
@ -432,8 +440,7 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
if (certificates.length > 1) {
return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
}
} catch (CertificateException ignored) {
}
} catch (CertificateException ignored) {}
return EncryptionUtil.certTrimNewLines(certificateValue);
}

26
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -91,6 +92,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -128,23 +130,21 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
validateId(deviceId, id -> INCORRECT_DEVICE_ID + id);
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return cache.get(new DeviceCacheKey(deviceId),
() -> deviceDao.findById(tenantId, deviceId.getId()));
} else {
return cache.get(new DeviceCacheKey(tenantId, deviceId),
() -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()));
}
return findDeviceByIdInternal(tenantId, deviceId);
}
@Override
public ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceByIdAsync [{}]", deviceId);
validateId(deviceId, id -> INCORRECT_DEVICE_ID + id);
return executor.submit(() -> findDeviceByIdInternal(tenantId, deviceId));
}
private Device findDeviceByIdInternal(TenantId tenantId, DeviceId deviceId) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return deviceDao.findByIdAsync(tenantId, deviceId.getId());
return cache.get(new DeviceCacheKey(deviceId), () -> deviceDao.findById(tenantId, deviceId.getId()));
} else {
return deviceDao.findDeviceByTenantIdAndIdAsync(tenantId, deviceId.getId());
return cache.get(new DeviceCacheKey(tenantId, deviceId), () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()));
}
}
@ -275,8 +275,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
}
}
@TransactionalEventListener(classes = DeviceCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(DeviceCacheEvictEvent event) {
List<DeviceCacheKey> toEvict = new ArrayList<>(3);
toEvict.add(new DeviceCacheKey(event.getTenantId(), event.getNewName()));
@ -748,6 +748,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
return Optional.ofNullable(findDeviceById(tenantId, new DeviceId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findDeviceByIdAsync(tenantId, new DeviceId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE;

16
dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.domain;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -39,17 +40,16 @@ import org.thingsboard.server.dao.service.validator.DomainDataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service
public class DomainServiceImpl extends AbstractEntityService implements DomainService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Autowired
private OAuth2ClientDao oauth2ClientDao;
@Autowired
@ -66,8 +66,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build());
return savedDomain;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("domain_name_key", "Domain with such name and scheme already exists!"));
checkConstraintViolation(e, "domain_name_key", "Domain with such name and scheme already exists!");
throw e;
}
}
@ -142,6 +141,12 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(domainDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -163,4 +168,5 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
public EntityType getEntityType() {
return EntityType.DOMAIN;
}
}

10
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.edge;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -83,6 +84,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -137,8 +139,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@TransactionalEventListener(classes = EdgeCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(EdgeCacheEvictEvent event) {
List<EdgeCacheKey> keys = new ArrayList<>(2);
keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName()));
@ -629,6 +631,12 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return Optional.ofNullable(findEdgeById(tenantId, new EdgeId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.EDGE;

20
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -67,15 +68,13 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_NAME;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE;
import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink;
import static org.thingsboard.server.dao.service.Validator.validateId;
/**
* Created by ashvayka on 04.05.17.
*/
@Service
@Slf4j
public class BaseEntityService extends AbstractEntityService implements EntityService {
@ -93,7 +92,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Autowired
@Lazy
EntityServiceRegistry entityServiceRegistry;
private EntityServiceRegistry entityServiceRegistry;
@Autowired
private EdqsService edqsService;
@ -198,6 +197,11 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
return fetchAndConvert(tenantId, entityId, this::getCustomerId);
}
@Override
public FluentFuture<Optional<CustomerId>> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId) {
return fetchAndConvertAsync(tenantId, entityId, this::getCustomerId);
}
@Override
public Optional<NameLabelAndCustomerDetails> fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) {
log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId);
@ -239,6 +243,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
return entityOpt.map(converter);
}
private <T> FluentFuture<Optional<T>> fetchAndConvertAsync(TenantId tenantId, EntityId entityId, Function<HasId<?>, T> converter) {
EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType());
return entityDaoService.findEntityAsync(tenantId, entityId)
.transform(entityOpt -> entityOpt.map(converter), directExecutor());
}
private String getName(HasId<?> entity) {
return entity instanceof HasName ? ((HasName) entity).getName() : null;
}
@ -329,7 +339,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
}
if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) &&
(query.getLatestValues() == null || query.getLatestValues().isEmpty())) {
(query.getLatestValues() == null || query.getLatestValues().isEmpty())) {
return false;
}

30
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.entityview;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -62,13 +63,11 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validateString;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@Service("EntityViewDaoService")
@Slf4j
public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityViewCacheKey, EntityViewCacheValue, EntityViewEvictEvent> implements EntityViewService {
@ -188,6 +187,17 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
public EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return findEntityViewByIdInternal(tenantId, entityViewId, putInCache);
}
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return service.submit(() -> findEntityViewByIdInternal(tenantId, entityViewId, true));
}
private EntityView findEntityViewByIdInternal(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) {
EntityViewCacheValue value = cache.get(EntityViewCacheKey.byId(entityViewId), () -> {
EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId());
return new EntityViewCacheValue(entityView, null);
@ -202,7 +212,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return cache.getAndPutInTransaction(EntityViewCacheKey.byName(tenantId, name),
() -> entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null)
, EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true);
}
@Override
@ -319,13 +328,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return entityViews;
}
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return entityViewDao.findByIdAsync(tenantId, entityViewId.getId());
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) {
log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId);
@ -491,6 +493,12 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return Optional.ofNullable(findEntityViewById(tenantId, new EntityViewId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findEntityViewByIdAsync(tenantId, new EntityViewId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;

18
dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.job;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -44,6 +45,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED;
import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED;
import static org.thingsboard.server.common.data.job.JobStatus.FAILED;
@ -87,11 +89,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
}
job.getResult().setCancellationTs(System.currentTimeMillis());
JobStatus prevStatus = job.getStatus();
if (job.getStatus() == QUEUED) {
job.setStatus(CANCELLED); // setting cancelled status right away, because we don't expect stats for cancelled tasks
} else if (job.getStatus() == PENDING) {
job.setStatus(RUNNING);
}
job.setStatus(CANCELLED);
saveJob(tenantId, job, true, prevStatus);
}
@ -145,7 +143,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
}
}
if (job.getStatus() == RUNNING) {
if (job.getStatus().isOneOf(RUNNING, CANCELLED)) {
if (result.getTotalCount() != null && result.getCompletedCount() >= result.getTotalCount()) {
if (result.getCancellationTs() > 0) {
job.setStatus(CANCELLED);
@ -193,7 +191,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
private void checkWaitingJobs(TenantId tenantId, JobType jobType) {
Job queuedJob = jobDao.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId, jobType, QUEUED);
if (queuedJob == null) {
if (queuedJob == null || jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, jobType, PENDING, RUNNING)) {
return;
}
queuedJob.setStatus(PENDING);
@ -245,6 +243,12 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
return Optional.ofNullable(findJobById(tenantId, (JobId) entityId));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(jobDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
jobDao.removeById(tenantId, id.getId());

15
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.mobile;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -40,11 +41,11 @@ import org.thingsboard.server.dao.service.DataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.checkNotNull;
@Slf4j
@ -60,7 +61,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
@Autowired
private DataValidator<MobileAppBundle> mobileAppBundleDataValidator;
@Override
public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) {
log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle);
@ -71,8 +71,8 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
return savedMobileApp;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!",
"mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!"));
"mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!",
"mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!");
throw e;
}
}
@ -149,6 +149,12 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(mobileAppBundleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -167,4 +173,5 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
.collect(Collectors.toList());
mobileAppBundleInfo.setOauth2ClientInfos(clients);
}
}

14
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.mobile;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -35,9 +36,10 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service
public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService {
@ -58,8 +60,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build());
return savedMobileApp;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!"));
checkConstraintViolation(e, "mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!");
throw e;
}
}
@ -88,6 +89,12 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(mobileAppDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -117,4 +124,5 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
public EntityType getEntityType() {
return EntityType.MOBILE_APP;
}
}

13
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
@ -38,6 +39,8 @@ import org.thingsboard.server.dao.service.DataValidator;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -129,13 +132,17 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
return Optional.ofNullable(findNotificationRequestById(tenantId, new NotificationRequestId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationRequestDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST;
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {}
}

14
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -33,9 +34,10 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@RequiredArgsConstructor
public class DefaultNotificationRuleService extends AbstractEntityService implements NotificationRuleService, EntityDaoService {
@ -56,9 +58,7 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem
.created(notificationRule.getId() == null).build());
return savedRule;
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_rule_name", "Notification rule with such name already exists"
));
checkConstraintViolation(e, "uq_notification_rule_name", "Notification rule with such name already exists");
throw e;
}
}
@ -114,6 +114,12 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem
return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationRuleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE;

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -37,6 +38,8 @@ import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import java.util.Optional;
import java.util.Set;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -95,6 +98,12 @@ public class DefaultNotificationService implements NotificationService, EntityDa
return Optional.ofNullable(findNotificationById(tenantId, new NotificationId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION;

14
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -43,10 +44,11 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.user.UserService;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -65,9 +67,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
.created(notificationTarget.getId() == null).build());
return savedTarget;
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_target_name", "Recipients group with such name already exists"
));
checkConstraintViolation(e, "uq_notification_target_name", "Recipients group with such name already exists");
throw e;
}
}
@ -180,6 +180,12 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationTargetDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET;

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -37,6 +38,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@RequiredArgsConstructor
public class DefaultNotificationTemplateService extends AbstractEntityService implements NotificationTemplateService, EntityDaoService {
@ -144,6 +147,12 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationTemplateDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE;

9
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.oauth2;
import com.google.common.util.concurrent.FluentFuture;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -41,6 +42,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service("OAuth2ClientService")
@ -107,7 +109,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
.tenantId(tenantId)
.entityId(oAuth2ClientId)
.build());
}
@Override
@ -149,6 +150,12 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
return Optional.ofNullable(findOAuth2ClientById(tenantId, new OAuth2ClientId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(oauth2ClientDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {

8
dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.ota;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -47,6 +48,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import java.nio.ByteBuffer;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -254,6 +256,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
return Optional.ofNullable(findOtaPackageInfoById(tenantId, new OtaPackageId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;

36
dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.queue;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
@ -42,6 +43,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("QueueDaoService")
@Slf4j
@RequiredArgsConstructor
@ -127,7 +131,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
@Override
public void deleteQueuesByTenantId(TenantId tenantId) {
Validator.validateId(tenantId, "Incorrect tenant id for delete queues request.");
validateId(tenantId, __ -> "Incorrect tenant id for delete queues request.");
tenantQueuesRemover.removeEntities(tenantId, tenantId);
}
@ -141,24 +145,30 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
return Optional.ofNullable(findQueueById(tenantId, new QueueId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(queueDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.QUEUE;
}
private PaginatedRemover<TenantId, Queue> tenantQueuesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<TenantId, Queue> tenantQueuesRemover = new PaginatedRemover<>() {
@Override
protected PageData<Queue> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return queueDao.findQueuesByTenantId(id, pageLink);
}
@Override
protected PageData<Queue> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return queueDao.findQueuesByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Queue entity) {
deleteQueue(tenantId, entity.getId());
}
};
@Override
protected void removeEntity(TenantId tenantId, Queue entity) {
deleteQueue(tenantId, entity.getId());
}
};
private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
@ -167,7 +177,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
return tenantId;
}
}
return TenantId.SYS_TENANT_ID;
}
}

8
dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.queue;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -35,6 +36,7 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -106,6 +108,12 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu
return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(queueStatsDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.QUEUE_STATS;

34
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -17,11 +17,11 @@ package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;
@ -356,8 +356,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
imageName = imageName + type + " image";
UpdateResult result = convertToImageUrl(entity.getTenantId(), imageName, entity.getImage(), Collections.emptyMap());
entity.setImage(result.getValue());
return result.isUpdated();
entity.setImage(result.value());
return result.updated();
}
@Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@ -373,8 +373,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
Map<String, String> imagesLinks = getResourcesLinks(widgetTypeDetails.getResources());
UpdateResult result = convertToImageUrl(tenantId, prefix + " image", widgetTypeDetails.getImage(), imagesLinks);
boolean updated = result.isUpdated();
widgetTypeDetails.setImage(result.getValue());
boolean updated = result.updated();
widgetTypeDetails.setImage(result.value());
if (widgetTypeDetails.getDescriptor().isObject()) {
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
@ -397,8 +397,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
Map<String, String> imagesLinks = getResourcesLinks(dashboard.getResources());
var result = convertToImageUrl(tenantId, prefix + " image", dashboard.getImage(), imagesLinks);
boolean updated = result.isUpdated();
dashboard.setImage(result.getValue());
boolean updated = result.updated();
dashboard.setImage(result.value());
updated |= convertToImageUrlsByMapping(tenantId, DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), dashboard.getConfiguration(), imagesLinks);
updated |= convertToImageUrls(tenantId, prefix, dashboard.getConfiguration(), imagesLinks);
@ -409,10 +409,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> {
UpdateResult result = convertToImageUrl(tenantId, name, value, links);
if (result.isUpdated()) {
if (result.updated()) {
updated.set(true);
}
return result.getValue();
return result.value();
});
return updated.get();
}
@ -514,10 +514,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAll(root, title, (path, value) -> {
UpdateResult result = convertToImageUrl(tenantId, path, value, true, links);
if (result.isUpdated()) {
if (result.updated()) {
updated.set(true);
}
return result.getValue();
return result.value();
});
return updated.get();
}
@ -678,16 +678,18 @@ public class BaseImageService extends BaseResourceService implements ImageServic
private String getImageLink(String value) {
if (value.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images")) {
return StringUtils.removeStart(value, DataConstants.TB_IMAGE_PREFIX);
return Strings.CS.removeStart(value, DataConstants.TB_IMAGE_PREFIX);
} else {
return null;
}
}
@Data(staticConstructor = "of")
private static class UpdateResult {
private final boolean updated;
private final String value;
private record UpdateResult(boolean updated, String value) {
static UpdateResult of(boolean updated, String value) {
return new UpdateResult(updated, value);
}
}
}

13
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@ -79,6 +80,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -107,7 +109,8 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao);
}
@Autowired @Lazy
@Autowired
@Lazy
private ImageService imageService;
private static final Map<String, String> DASHBOARD_RESOURCES_MAPPING = Map.of(
@ -275,7 +278,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public TbResource toResource(TenantId tenantId, ResourceExportData exportData) {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
throw new IllegalArgumentException("Image import not supported");
}
@ -457,6 +460,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return Optional.ofNullable(findResourceInfoById(tenantId, new TbResourceId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findResourceInfoByIdAsync(tenantId, new TbResourceId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;

35
dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.rpc;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -33,6 +34,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -40,6 +42,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Slf4j
@RequiredArgsConstructor
public class BaseRpcService implements RpcService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RPC_ID = "Incorrect rpcId ";
@ -113,21 +116,29 @@ public class BaseRpcService implements RpcService {
return Optional.ofNullable(findById(tenantId, new RpcId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findRpcByIdAsync(tenantId, new RpcId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.RPC;
}
private PaginatedRemover<TenantId, Rpc> tenantRpcRemover =
new PaginatedRemover<>() {
@Override
protected PageData<Rpc> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return rpcDao.findAllRpcByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Rpc entity) {
deleteRpc(tenantId, entity.getId());
}
};
private final PaginatedRemover<TenantId, Rpc> tenantRpcRemover = new PaginatedRemover<>() {
@Override
protected PageData<Rpc> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return rpcDao.findAllRpcByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Rpc entity) {
deleteRpc(tenantId, entity.getId());
}
};
}

38
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.rule;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@ -28,6 +29,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.id.HasId;
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.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
@ -80,6 +83,7 @@ import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.DataConstants.TENANT;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -87,9 +91,6 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber;
import static org.thingsboard.server.dao.service.Validator.validateString;
/**
* Created by igor on 3/12/18.
*/
@Service("RuleChainDaoService")
@Slf4j
public class BaseRuleChainService extends AbstractEntityService implements RuleChainService {
@ -98,6 +99,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String TB_RULE_CHAIN_INPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainInputNode";
@Autowired
private RuleChainDao ruleChainDao;
@ -260,7 +262,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId();
}
if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
|| (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
|| (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
ruleChain.setFirstRuleNodeId(firstRuleNodeId);
}
if (ruleChainMetaData.getConnections() != null) {
@ -876,6 +878,17 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return Optional.ofNullable(hasId);
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
ListenableFuture<? extends BaseDataWithAdditionalInfo<? extends UUIDBased>> future;
if (entityId.getEntityType() == EntityType.RULE_NODE) {
future = findRuleNodeByIdAsync(tenantId, new RuleNodeId(entityId.getId()));
} else {
future = findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId()));
}
return FluentFuture.from(future).transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return ruleChainDao.countByTenantId(tenantId);
@ -919,18 +932,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "clusteringMode");
switch (nodeConfigType) {
case ENABLED:
singletonMode = false;
break;
case SINGLETON:
singletonMode = true;
break;
case USER_PREFERENCE:
default:
singletonMode = ruleNode.isSingletonMode();
break;
}
singletonMode = switch (nodeConfigType) {
case ENABLED -> false;
case SINGLETON -> true;
default -> ruleNode.isSingletonMode();
};
} catch (Exception e) {
log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e));
singletonMode = false;

9
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.settings;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -31,6 +32,8 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
public class AdminSettingsServiceImpl implements AdminSettingsService {
@ -106,6 +109,12 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId()));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(adminSettingsDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ADMIN_SETTINGS;

5
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java

@ -136,6 +136,11 @@ public class JpaUserDao extends JpaAbstractDao<UserEntity, User> implements User
DaoUtil.toPageable(pageLink)));
}
@Override
public int countTenantAdmins(UUID tenantId) {
return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN);
}
@Override
public Long countByTenantId(TenantId tenantId) {
return userRepository.countByTenantId(tenantId.getId());

2
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java

@ -78,4 +78,6 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
"u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " +
"FROM UserEntity u WHERE u.id > :id ORDER BY u.id")
List<UserFields> findNextBatch(@Param("id") UUID id, Limit limit);
int countByTenantIdAndAuthority(UUID tenantId, Authority authority);
}

30
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.tenant;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +45,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -230,23 +232,29 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService<Tenant
return Optional.ofNullable(findTenantProfileById(tenantId, new TenantProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(tenantProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TENANT_PROFILE;
}
private final PaginatedRemover<String, TenantProfile> tenantProfilesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<String, TenantProfile> tenantProfilesRemover = new PaginatedRemover<>() {
@Override
protected PageData<TenantProfile> findEntities(TenantId tenantId, String id, PageLink pageLink) {
return tenantProfileDao.findTenantProfiles(tenantId, pageLink);
}
@Override
protected PageData<TenantProfile> findEntities(TenantId tenantId, String id, PageLink pageLink) {
return tenantProfileDao.findTenantProfiles(tenantId, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, TenantProfile entity) {
removeTenantProfile(tenantId, entity, entity.isDefault());
}
@Override
protected void removeEntity(TenantId tenantId, TenantProfile entity) {
removeTenantProfile(tenantId, entity, entity.isDefault());
}
};
};
}

10
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.tenant;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -51,6 +52,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TenantDaoService")
@ -86,8 +88,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
protected TbTransactionalCache<TenantId, Boolean> existsTenantCache;
@TransactionalEventListener(classes = TenantEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(TenantEvictEvent event) {
TenantId tenantId = event.getTenantId();
cache.evict(tenantId);
@ -246,6 +248,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
return Optional.ofNullable(findTenantById(TenantId.fromUUID(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findTenantByIdAsync(tenantId, TenantId.fromUUID(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TENANT;

11
dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.usagerecord;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -48,11 +49,13 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("ApiUsageStateDaoService")
@Slf4j
public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private final ApiUsageStateDao apiUsageStateDao;
@ -161,7 +164,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
public ApiUsageState update(ApiUsageState apiUsageState) {
log.trace("Executing save [{}]", apiUsageState.getTenantId());
validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id);
validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!");
validateId(apiUsageState.getId(), __ -> "Can't save new usage state. Only update is allowed!");
apiUsageState.setVersion(null);
ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId())
@ -195,6 +198,12 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
return Optional.ofNullable(findApiUsageStateById(tenantId, new ApiUsageStateId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(apiUsageStateDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Transactional
@Override
public void deleteByTenantId(TenantId tenantId) {

1
dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java

@ -101,4 +101,5 @@ public interface UserDao extends Dao<User>, TenantEntityDao<User> {
PageData<User> findByAuthorityAndTenantProfilesIds(Authority authority, List<TenantProfileId> tenantProfilesIds, PageLink pageLink);
int countTenantAdmins(UUID tenantId);
}

15
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.user;
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.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -79,6 +80,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -112,8 +114,8 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
private final EntityCountService countService;
private final JpaExecutorService executor;
@TransactionalEventListener(classes = UserCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(UserCacheEvictEvent event) {
List<UserCacheKey> keys = new ArrayList<>(2);
keys.add(new UserCacheKey(event.tenantId(), event.newEmail()));
@ -494,6 +496,11 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
}
}
@Override
public int countTenantAdmins(TenantId tenantId) {
return userDao.countTenantAdmins(tenantId.getId());
}
private Optional<UserMobileSessionInfo> findMobileSessionInfo(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE))
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class));
@ -646,6 +653,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return Optional.ofNullable(findUserById(tenantId, new UserId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findUserByIdAsync(tenantId, new UserId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return userDao.countByTenantId(tenantId);

12
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.widget;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -47,6 +48,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@Service("WidgetTypeDaoService")
@ -54,8 +56,6 @@ import static org.thingsboard.server.dao.service.Validator.validateIds;
public class WidgetTypeServiceImpl implements WidgetTypeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias ";
public static final String INCORRECT_WIDGETS_BUNDLE_ID = "Incorrect widgetsBundleId ";
@Autowired
@ -281,6 +281,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return Optional.ofNullable(findWidgetTypeById(tenantId, new WidgetTypeId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(widgetTypeDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.WIDGET_TYPE;
@ -303,6 +309,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) {
deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId()));
}
};
private final PaginatedRemover<WidgetsBundleId, WidgetTypeInfo> bundleWidgetTypesRemover = new PaginatedRemover<>() {
@ -316,6 +323,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
protected void removeEntity(TenantId tenantId, WidgetTypeInfo widgetTypeInfo) {
deleteWidgetType(tenantId, widgetTypeInfo.getId());
}
};
}

48
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.widget;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@ -33,12 +34,10 @@ import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
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.resource.ResourceService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -48,13 +47,15 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation;
@Service("WidgetsBundleDaoService")
@Slf4j
public class WidgetsBundleServiceImpl implements WidgetsBundleService {
private static final int DEFAULT_WIDGETS_BUNDLE_LIMIT = 300;
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
@Autowired
private WidgetsBundleDao widgetsBundleDao;
@ -71,9 +72,6 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
@Autowired
private ImageService imageService;
@Autowired
private ResourceService resourceService;
@Override
public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) {
log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId);
@ -92,9 +90,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
.entityId(result.getId()).created(widgetsBundle.getId() == null).build());
return result;
} catch (Exception e) {
AbstractCachedEntityService.checkConstraintViolation(e,
"uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!");
AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!");
checkConstraintViolation(e,
"uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!",
"widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!");
throw e;
}
}
@ -248,9 +246,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
if (widgetsBundleDescriptor.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptor.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
widgetFqnsArrayJson.forEach(fqnJson -> widgetTypeFqns.add(fqnJson.asText()));
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), widgetTypeFqns);
});
@ -274,23 +270,29 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(widgetsBundleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.WIDGETS_BUNDLE;
}
private PaginatedRemover<TenantId, WidgetsBundle> tenantWidgetsBundleRemover =
new PaginatedRemover<TenantId, WidgetsBundle>() {
private final PaginatedRemover<TenantId, WidgetsBundle> tenantWidgetsBundleRemover = new PaginatedRemover<>() {
@Override
protected PageData<WidgetsBundle> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<WidgetsBundle> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, WidgetsBundle entity) {
deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId()));
}
@Override
protected void removeEntity(TenantId tenantId, WidgetsBundle entity) {
deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId()));
}
};
};
}

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java

@ -58,15 +58,9 @@ public abstract class TbAbstractGetEntityDataNode<T extends EntityId> extends Tb
protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) {
DataToFetch dataToFetch = config.getDataToFetch();
switch (dataToFetch) {
case ATTRIBUTES:
processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case LATEST_TELEMETRY:
processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case FIELDS:
processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true);
break;
case ATTRIBUTES -> processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
case LATEST_TELEMETRY -> processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
case FIELDS -> processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true);
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java

@ -17,8 +17,7 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.base.Function;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
@ -54,12 +53,12 @@ public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeC
protected abstract C loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected <I extends EntityId> AsyncFunction<I, I> checkIfEntityIsPresentOrThrow(String message) {
protected <I extends EntityId> Function<I, I> checkIfEntityIsPresentOrThrow(String message) {
return id -> {
if (id == null || id.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException(message));
throw new NoSuchElementException(message);
}
return Futures.immediateFuture(id);
return id;
};
}

26
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -16,19 +16,22 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import java.util.NoSuchElementException;
import static com.google.common.util.concurrent.Futures.immediateFuture;
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "customer attributes",
@ -44,8 +47,6 @@ import org.thingsboard.server.common.data.util.TbPair;
)
public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<CustomerId> {
private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s";
@Override
protected TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class);
@ -56,10 +57,19 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<Cust
@Override
protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
return Futures.transformAsync(EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator),
checkIfEntityIsPresentOrThrow(String.format(CUSTOMER_NOT_FOUND_MESSAGE, originator.getId(), originator.getEntityType().getNormalName())),
ctx.getDbCallbackExecutor()
);
if (originator.getEntityType() == EntityType.CUSTOMER) {
return immediateFuture((CustomerId) originator);
}
return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), originator)
.transform(customerIdOpt -> {
if (customerIdOpt.isEmpty()) {
throw new NoSuchElementException("Originator not found");
}
if (customerIdOpt.get().isNullUid()) {
throw new IllegalStateException("Originator is not assigned to any customer");
}
return customerIdOpt.get();
}, ctx.getDbCallbackExecutor());
}
@Override

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java

@ -55,7 +55,7 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDevice
@Override
protected ListenableFuture<DeviceId> findEntityIdAsync(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(
return Futures.transform(
EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_DEVICE_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -58,7 +58,7 @@ public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode<Entit
@Override
public ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
var relatedAttrConfig = (TbGetRelatedDataNodeConfiguration) config;
return Futures.transformAsync(
return Futures.transform(
EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, relatedAttrConfig.getRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_ENTITY_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesAlarmOriginatorIdAsyncLoader;
import org.thingsboard.rule.engine.util.EntitiesByNameAndTypeLoader;
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@ -36,6 +35,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
import java.util.NoSuchElementException;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY;
import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED;
@ -74,16 +74,20 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOrig
if (newOriginator == null || newOriginator.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException("Failed to find new originator!"));
}
return Futures.immediateFuture(List.of(ctx.transformMsgOriginator(msg, newOriginator)));
return immediateFuture(List.of(ctx.transformMsgOriginator(msg, newOriginator)));
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, TbMsg msg) {
switch (config.getOriginatorSource()) {
case CUSTOMER:
return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, msg.getOriginator());
if (msg.getOriginator().getEntityType() == EntityType.CUSTOMER) {
return immediateFuture(msg.getOriginator());
}
return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), msg.getOriginator())
.transform(customerIdOpt -> customerIdOpt.orElse(null), ctx.getDbCallbackExecutor());
case TENANT:
return Futures.immediateFuture(ctx.getTenantId());
return immediateFuture(ctx.getTenantId());
case RELATED:
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, msg.getOriginator(), config.getRelationsQuery());
case ALARM_ORIGINATOR:
@ -93,7 +97,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOrig
String entityName = TbNodeUtils.processPattern(config.getEntityNamePattern(), msg);
try {
EntityId targetEntity = EntitiesByNameAndTypeLoader.findEntityId(ctx, entityType, entityName);
return Futures.immediateFuture(targetEntity);
return immediateFuture(targetEntity);
} catch (IllegalStateException e) {
return Futures.immediateFailedFuture(e);
}

50
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java

@ -1,50 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
public class EntitiesCustomerIdAsyncLoader {
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId originator) {
switch (originator.getEntityType()) {
case CUSTOMER:
return Futures.immediateFuture((CustomerId) originator);
case USER:
return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator));
case ASSET:
return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator));
case DEVICE:
return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator)));
default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType()));
}
}
private static <T extends HasCustomerId> ListenableFuture<CustomerId> toCustomerIdAsync(TbContext ctx, ListenableFuture<T> future) {
return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor());
}
}

2
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java

@ -28,7 +28,7 @@ public class TestDbCallbackExecutor implements ListeningExecutor {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
return Futures.immediateFailedFuture(e);
}
}

111
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -16,7 +16,7 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -53,19 +53,19 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@ -73,19 +73,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbGetCustomerAttributeNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID());
private final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
@ -93,21 +94,24 @@ public class TbGetCustomerAttributeNodeTest {
@Mock
private TimeseriesService timeseriesServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
private EntityService entityServiceMock;
private TbGetCustomerAttributeNode node;
private TbGetEntityDataNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
@BeforeEach
public void setUp() {
public void setup() {
node = new TbGetCustomerAttributeNode();
config = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
lenient().when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
lenient().when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock);
}
@Test
@ -237,12 +241,9 @@ public class TbGetCustomerAttributeNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(null)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(userId));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, userId)).thenReturn(
FluentFuture.from(immediateFuture(Optional.empty()))
);
// WHEN
node.onMsg(ctxMock, msg);
@ -252,18 +253,13 @@ public class TbGetCustomerAttributeNodeTest {
var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1))
.tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
verify(ctxMock).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
var actualMessage = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
var expectedExceptionMessage = String.format(
"Failed to find customer for entity with id: %s and type: %s",
userId.getId(), userId.getEntityType().getNormalName());
assertEquals(msg, actualMessage);
assertEquals(expectedExceptionMessage, actualException.getMessage());
assertEquals("Originator not found", actualException.getMessage());
assertInstanceOf(NoSuchElementException.class, actualException);
}
@ -282,16 +278,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId()));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(attributesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -299,7 +291,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
@ -329,16 +321,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId()));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, user.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(attributesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -346,7 +334,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
@ -375,21 +363,18 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(customer.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(timeseriesList));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyNoInteractions(entityServiceMock);
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
@ -408,7 +393,7 @@ public class TbGetCustomerAttributeNodeTest {
public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
asset.setCustomerId(new CustomerId(UUID.randomUUID()));
asset.setCustomerId(CUSTOMER_ID);
prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId());
@ -419,16 +404,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()));
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(asset.getCustomerId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, asset.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(CUSTOMER_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(immediateFuture(timeseriesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -436,7 +417,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(

90
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java

@ -15,8 +15,10 @@
*/
package org.thingsboard.rule.engine.transform;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
@ -35,6 +37,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -54,21 +57,24 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ALARM_ORIGINATOR;
import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY;
@ -93,16 +99,23 @@ public class TbChangeOriginatorNodeTest {
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private RelationService relationServiceMock;
@Mock
private RuleEngineAlarmService alarmServiceMock;
@Mock
private EntityService entityServiceMock;
@BeforeEach
public void before() throws TbNodeException {
public void setup() {
node = new TbChangeOriginatorNode();
config = new TbChangeOriginatorNodeConfiguration().defaultConfiguration();
lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
lenient().when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
lenient().when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
lenient().when(ctxMock.getAlarmService()).thenReturn(alarmServiceMock);
lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock);
}
@Test
@ -172,8 +185,13 @@ public class TbChangeOriginatorNodeTest {
}
@Test
public void givenOriginatorSourceIsCustomer_whenOnMsg_thenTellSuccess() throws TbNodeException {
Device device = new Device(DEVICE_ID);
@DisplayName("""
Given a device assigned to a customer and node configured to change originator to customer,
when processing the message,
then should change message originator from device to customer""")
public void givenDeviceAssignedToCustomer_whenProcessingMessage_thenChangesOriginatorToCustomer() throws TbNodeException {
// GIVEN
var device = new Device(DEVICE_ID);
device.setCustomerId(CUSTOMER_ID);
TbMsg msg = TbMsg.newMsg()
@ -186,16 +204,52 @@ public class TbChangeOriginatorNodeTest {
.originator(CUSTOMER_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getDeviceService()).willReturn(deviceServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(deviceServiceMock.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(device);
given(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).willReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID);
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg);
}
@Test
@DisplayName("""
Given a customer as message originator and node configured to change originator to customer,
when processing the message,
then should keep the customer as originator""")
public void givenCustomerAsOriginator_whenProcessingMessage_thenKeepsCustomerAsOriginator() throws TbNodeException {
// GIVEN
var customer = new Customer(CUSTOMER_ID);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(customer.getId())
.metaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
var expectedMsg = msg.transform()
.originator(CUSTOMER_ID)
.build();
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
// WHEN
node.onMsg(ctxMock, msg);
then(deviceServiceMock).should().findDeviceById(TENANT_ID, DEVICE_ID);
// THEN
then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID);
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
@ -216,8 +270,6 @@ public class TbChangeOriginatorNodeTest {
.originator(TENANT_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
@ -240,9 +292,6 @@ public class TbChangeOriginatorNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getRelationService()).willReturn(relationServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(relationServiceMock.findByQuery(any(TenantId.class), any(EntityRelationsQuery.class))).willReturn(Futures.immediateFuture(Collections.emptyList()));
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
@ -282,9 +331,6 @@ public class TbChangeOriginatorNodeTest {
.originator(DEVICE_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAlarmService()).willReturn(alarmServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(alarmServiceMock.findAlarmByIdAsync(any(TenantId.class), any(AlarmId.class))).willReturn(Futures.immediateFuture(alarm));
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
@ -315,9 +361,6 @@ public class TbChangeOriginatorNodeTest {
.originator(ASSET_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAssetService()).willReturn(assetServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(new Asset(ASSET_ID));
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
@ -355,9 +398,6 @@ public class TbChangeOriginatorNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAssetService()).willReturn(assetServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(null);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));

154
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java

@ -1,154 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.user.UserService;
import java.util.EnumSet;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class EntitiesCustomerIdAsyncLoaderTest {
private static final EnumSet<EntityType> SUPPORTED_ENTITY_TYPES = EnumSet.of(
EntityType.CUSTOMER,
EntityType.USER,
EntityType.ASSET,
EntityType.DEVICE
);
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Test
public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var customer = new Customer(new CustomerId(UUID.randomUUID()));
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get();
// THEN
assertEquals(customer.getId(), actualCustomerId);
}
@Test
public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var user = new User(new UserId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
user.setCustomerId(expectedCustomerId);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
asset.setCustomerId(expectedCustomerId);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var device = new Device(new DeviceId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
device.setCustomerId(expectedCustomerId);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() {
for (var entityType : EntityType.values()) {
if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) {
var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID());
var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType;
var exception = assertThrows(ExecutionException.class,
() -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get());
assertInstanceOf(TbNodeException.class, exception.getCause());
assertEquals(expectedExceptionMsg, exception.getMessage());
}
}
}
}

2
ui-ngx/src/app/core/http/admin.service.ts

@ -21,9 +21,9 @@ import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
AutoCommitSettings,
MailConfigTemplate,
FeaturesInfo,
JwtSettings,
MailConfigTemplate,
MailServerSettings,
RepositorySettings,
RepositorySettingsInfo,

38
ui-ngx/src/app/core/http/git-hub.service.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class GitHubService {
constructor(
private http: HttpClient
) { }
public getGitHubStar(config?: RequestConfig): Observable<number> {
return this.http.get<any>('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe(
catchError(() => of({})),
map((res: any) => res?.stargazers_count ?? 0)
)
}
}

10
ui-ngx/src/app/core/utils.ts

@ -34,8 +34,10 @@ import {
} from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
const varsRegex = /\${([^}]*)}/g;
const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
const scrollSubject = new Subject<Event>();
@ -1033,3 +1035,11 @@ export const trimDefaultValues = (input: Record<string, any>, defaults: Record<s
return result;
}
export const validateEmail = (control: AbstractControl): ValidationErrors | null => {
if (isUndefinedOrNull(control.value) || (typeof control.value === 'string' && control.value.length === 0)) {
return null;
}
return emailRegex.test(control.value) ? null : {email: true};
};

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -649,9 +649,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
private hideToolbarSetting(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.hideToolbar)) {
return this.dashboard.configuration.settings.hideToolbar;
if (isDefined(this.dashboard.configuration?.settings?.hideToolbar)) {
const canApplyHideSetting = !this.forceFullscreen || this.isMobileApp;
return this.dashboard.configuration.settings.hideToolbar && canApplyHideSetting;
} else {
return false;
}

3
ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts

@ -24,6 +24,7 @@ import { EntityComponent } from './entity.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { CountryData } from '@shared/models/country.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { validateEmail } from '@app/core/utils';
@Directive()
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
@ -50,7 +51,7 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
entityForm.addControl('address', this.fb.control(entity ? entity.address : '', []));
entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', []));
entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [Validators.maxLength(255)]));
entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email]));
entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [validateEmail]));
return entityForm;
}

1
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -45,6 +45,7 @@
</tb-entity-type-select>
<tb-entity-list required
appearance="outline"
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="filterFormGroup.get('entityType').value"
formControlName="entityList">

14
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
<mat-toolbar color="primary">
<h2>{{ filter.filter }}</h2>
<h2>{{ filter.filter | customTranslate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
@ -33,14 +33,14 @@
*ngFor="let userInputControl of userInputsFormArray().controls; let $index = index">
<div [ngSwitch]="userInputControl.get('valueType').value">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<mat-form-field class="mat-block">
<mat-label>{{ userInputControl.get('label').value }}</mat-label>
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label>
<input matInput [formControl]="userInputControl.get('value')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field class="mat-block">
<mat-label>{{ userInputControl.get('label').value }}</mat-label>
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{ userInputControl.get('label').value | customTranslate }}</mat-label>
<input required type="number" matInput [formControl]="userInputControl.get('value')">
<div matSuffix class="mr-2"
[class.!hidden]="!userInputControl.get('unitSymbol').value">
@ -49,14 +49,14 @@
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value | customTranslate }}</label>
<tb-datetime [formControl]="userInputControl.get('value')"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
<mat-checkbox labelPosition="before" [formControl]="userInputControl.get('value')">
{{ userInputControl.get('label').value }}
{{ userInputControl.get('label').value | customTranslate }}
</mat-checkbox>
</ng-template>
</div>

26
ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
@if (githubStar > 0) {
<section class="flex size-full items-center gap-4">
<a mat-stroked-button href="https://github.com/thingsboard/thingsboard" target="_blank">
<tb-icon class="tb-mat-20" matButtonIcon>mdi:github</tb-icon>
<div class="button-label flex gap-1">GitHub<tb-icon class="tb-mat-14 !mr-0">star</tb-icon>{{ githubStar | number }}</div>
</a>
<mat-divider vertical class="h-full"></mat-divider>
</section>
}

35
ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 {
&:has(section) {
height: 100%;
}
--mat-outlined-button-horizontal-padding: 8px;
--mdc-outlined-button-container-height: 30px;
--mdc-outlined-button-label-text-size: 12px;
.mdc-button {
background-color: rgba(255, 255, 255, 0.12);
&:hover {
background-color: rgba(255, 255, 255, 0.20);
}
}
.button-label {
margin-top: 1px;
}
}

34
ui-ngx/src/app/modules/home/components/github-badge/github-badge.component.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT 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 } from '@angular/core';
import { GitHubService } from '@core/http/git-hub.service';
@Component({
selector: 'tb-github-badge',
templateUrl: './github-badge.component.html',
styleUrl: './github-badge.component.scss'
})
export class GithubBadgeComponent {
githubStar = 0;
constructor(private gitHubService: GitHubService) {
this.gitHubService.getGitHubStar().subscribe(star => {
this.githubStar = star;
});
}
}

6
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html

@ -38,7 +38,7 @@
<mat-expansion-panel class="entity-type-config flex-1" [formGroup]="entityTypeFormGroup" [expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<mat-panel-title class="basis-auto">
<div class="flex flex-1 flex-row items-center justify-start">
<div [innerHTML]="entityTypeText(entityTypeFormGroup)"></div>
</div>
@ -60,6 +60,7 @@
<div class="flex flex-row gap-4 xs:flex-col xs:gap-0">
<tb-entity-type-select
showLabel
appearance="outline"
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
@ -67,13 +68,14 @@
</tb-entity-type-select>
<div formGroupName="config">
<tb-branch-autocomplete
appearance="outline"
emptyPlaceholder="{{ 'version-control.default' | translate }}"
[selectDefaultBranch]="false"
formControlName="branch">
</tb-branch-autocomplete>
</div>
</div>
<div class="flex flex-1 flex-row items-center justify-start gap-4 xs:flex-col xs:items-start xs:justify-start xs:gap-0"
<div class="flex flex-1 flex-col gt-xs:flex-row items-start gt-xs:items-center justify-start gap-1 gt-xs:gap-4"
formGroupName="config">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="saveCredentials">
{{ 'version-control.export-credentials' | translate }}

1
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss

@ -52,7 +52,6 @@
padding: 0 8px 8px;
tb-branch-autocomplete {
min-width: 200px;
max-width: 200px;
display: block;
}
}

2
ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html

@ -41,7 +41,7 @@
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block" style="margin-bottom: 24px;" appearance="outline">
<mat-form-field class="mat-block" style="margin-bottom: 24px;" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
@for (strategy of syncStrategies; track strategy) {

9
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -330,6 +330,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (this.displayPagination) {
this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.paginator.pageIndex = 0);
this.ctx.aliasController?.filtersChanged.pipe(
takeUntil(this.destroy$)
).subscribe((filters) => {
let currentFilterId = this.ctx.defaultSubscription.options.alarmSource?.filterId;
if (currentFilterId && filters.includes(currentFilterId)) {
this.paginator.firstPage();
}
});
}
((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable<any>).pipe(
takeUntil(this.destroy$)

9
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -265,6 +265,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.displayPagination) {
this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.paginator.pageIndex = 0);
this.ctx.aliasController?.filtersChanged.pipe(
takeUntil(this.destroy$)
).subscribe((filters) => {
let currentFilterId = this.ctx.defaultSubscription.options.datasources?.[0]?.filterId;
if (currentFilterId && filters.includes(currentFilterId)) {
this.paginator.firstPage();
}
});
}
((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable<any>).pipe(
takeUntil(this.destroy$)

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.html

@ -17,7 +17,7 @@
-->
<div class="tb-led-indicator flex flex-col">
<div #ledTitleContainer class="title-container flex max-h-20% flex-full flex-row items-center justify-center" [class.!hidden]="!showTitle">
<span #ledTitle class="led-title">{{title}}</span>
<span #ledTitle class="led-title">{{title | customTranslate}}</span>
</div>
<div class="flex flex-full flex-col items-center justify-center"
[class]="{

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html

@ -30,8 +30,13 @@
<div *ngIf="dataLayerType === 'polylines'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polyline.polyline-key</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body">
<div *ngFor="let dataLayerControl of dataLayersFormArray().controls; trackBy: trackByDataLayer; let $index = index;">
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body tb-drop-list max-h-64 overflow-y-auto"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="layerDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-draggable-form-table-row"
*ngFor="let dataLayerControl of dataLayersFormArray().controls; trackBy: trackByDataLayer; let $index = index;">
<tb-map-data-layer-row class="flex-1"
[mapType]="mapType"
[dataLayerType]="dataLayerType"
@ -39,6 +44,16 @@
[formControl]="dataLayerControl"
(dataLayerRemoved)="removeDataLayer($index)">
</tb-map-data-layer-row>
<div class="tb-form-table-row-cell-buttons">
<button [class.tb-hidden]="!dragEnabled"
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts

@ -38,6 +38,7 @@ import {
} from '@shared/models/widget/maps/map.models';
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({
selector: 'tb-map-data-layers',
@ -79,6 +80,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
noDataLayersText: string;
get dragEnabled(): boolean {
return this.dataLayersFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {};
constructor(private mapSettingsComponent: MapSettingsComponent,
@ -166,6 +171,13 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
removeDataLayer(index: number) {
(this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index);
}
layerDrop(event: CdkDragDrop<string[]>) {
const layersArray = this.dataLayersFormArray();
const layer = layersArray.at(event.previousIndex);
layersArray.removeAt(event.previousIndex);
layersArray.insert(event.currentIndex, layer);
}
addDataLayer() {
const dataLayer = mergeDeep<MapDataLayerSettings>({} as MapDataLayerSettings,

1
ui-ngx/src/app/modules/home/home.component.html

@ -62,6 +62,7 @@
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
</div>
<tb-github-badge class="lt-md:!hidden"></tb-github-badge>
<button [class.!hidden]="!searchEnabled"
mat-icon-button (click)="openSearch()">
<mat-icon class="material-icons">search</mat-icon>

2
ui-ngx/src/app/modules/home/home.module.ts

@ -23,6 +23,7 @@ import { SharedModule } from '@app/shared/shared.module';
import { MenuLinkComponent } from '@modules/home/menu/menu-link.component';
import { MenuToggleComponent } from '@modules/home/menu/menu-toggle.component';
import { SideMenuComponent } from '@modules/home/menu/side-menu.component';
import { GithubBadgeComponent } from '@home/components/github-badge/github-badge.component';
import { NotificationBellComponent } from '@home/components/notification/notification-bell.component';
import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component';
@ -33,6 +34,7 @@ import { ShowNotificationPopoverComponent } from '@home/components/notification/
MenuLinkComponent,
MenuToggleComponent,
SideMenuComponent,
GithubBadgeComponent,
NotificationBellComponent,
ShowNotificationPopoverComponent
],

3
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html

@ -45,6 +45,9 @@
<mat-label translate>admin.user-lockout-notification-email</mat-label>
<input matInput type="email"
formControlName="userLockoutNotificationEmail"/>
<mat-error *ngIf="securitySettingsFormGroup.get('userLockoutNotificationEmail').invalid">
{{ 'login.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.user-activation-token-ttl</mat-label>

4
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts

@ -32,7 +32,7 @@ import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { mergeMap, tap } from 'rxjs/operators';
import { randomAlphanumeric } from '@core/utils';
import { randomAlphanumeric, validateEmail } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
@ -76,7 +76,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
buildSecuritySettingsForm() {
this.securitySettingsFormGroup = this.fb.group({
maxFailedLoginAttempts: [null, [Validators.min(0)]],
userLockoutNotificationEmail: ['', []],
userLockoutNotificationEmail: ['', [validateEmail]],
userActivationTokenTtl: [24, [Validators.required, Validators.min(1), Validators.max(24)]],
passwordResetTokenTtl: [24, [Validators.required, Validators.min(1), Validators.max(24)]],
mobileSecretKeyLength: [null, [Validators.min(1)]],

8
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -171,11 +171,11 @@
</div>
</div>
<mat-divider class="divider"></mat-divider>
<mat-chip-listbox>
<mat-chip *ngFor="let recipientTitle of preview.recipientsPreview">
<mat-chip-set>
<mat-chip-row class="tb-chip-row-ellipsis" *ngFor="let recipientTitle of preview.recipientsPreview">
<span>{{ recipientTitle }}</span>
</mat-chip>
</mat-chip-listbox>
</mat-chip-row>
</mat-chip-set>
</section>
<section class="preview-group notification tb-form-panel stroked no-gap" *ngIf="preview.processedTemplates.WEB?.enabled">
<div class="flex flex-row items-center justify-start gap-2">

2
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -37,7 +37,7 @@
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>user.email</mat-label>
<input matInput type="email" formControlName="email" required/>
<input matInput type="email" formControlName="email"/>
<mat-error *ngIf="profile.get('email').hasError('required')">
{{ 'user.email-required' | translate }}
</mat-error>

4
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -27,7 +27,7 @@ import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { isDefinedAndNotNull, isNotEmptyStr, validateEmail } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { AuthService } from '@core/auth/auth.service';
import { UnitSystem, UnitSystems } from '@shared/models/unit.models';
@ -64,7 +64,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private buildProfileForm() {
this.profile = this.fb.group({
email: ['', [Validators.required, Validators.email]],
email: ['', [Validators.required, validateEmail]],
firstName: [''],
lastName: [''],
phone: [''],

2
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html

@ -39,7 +39,7 @@
<div class="flex flex-row items-center justify-between gap-2">
<mat-form-field class="mat-block input-container flex-1">
<input matInput formControlName="email"
type="email" required
type="email"
placeholder="{{ 'security.2fa.dialog.email-step-label' | translate }}" />
<mat-error *ngIf="emailConfigForm.get('email').hasError('required')">
{{ 'user.email-required' | translate }}

3
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts

@ -28,6 +28,7 @@ import {
TwoFactorAuthProviderType
} from '@shared/models/two-factor-auth.models';
import { MatStepper } from '@angular/material/stepper';
import { validateEmail } from '@app/core/utils';
export interface EmailAuthDialogData {
email: string;
@ -57,7 +58,7 @@ export class EmailAuthDialogComponent extends DialogComponent<EmailAuthDialogCom
super(store, router, dialogRef);
this.emailConfigForm = this.fb.group({
email: [this.data.email, [Validators.required, Validators.email]]
email: [this.data.email, [Validators.required, validateEmail]]
});
this.emailVerificationForm = this.fb.group({

2
ui-ngx/src/app/modules/home/pages/user/user.component.html

@ -76,7 +76,7 @@
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>user.email</mat-label>
<input matInput type="email" formControlName="email" required>
<input matInput type="email" formControlName="email">
<mat-error *ngIf="entityForm.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>

6
ui-ngx/src/app/modules/home/pages/user/user.component.ts

@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
import { selectAuth } from '@core/auth/auth.selectors';
import { map } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
import { isDefinedAndNotNull } from '@core/utils';
import { isDefinedAndNotNull, validateEmail } from '@core/utils';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { ActionNotificationShow } from '@app/core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
@ -35,7 +35,7 @@ import { UnitSystems } from '@shared/models/unit.models';
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent extends EntityComponent<User> {
export class UserComponent extends EntityComponent<User>{
authority = Authority;
languageList = env.supportedLangs;
@ -74,7 +74,7 @@ export class UserComponent extends EntityComponent<User> {
buildForm(entity: User): UntypedFormGroup {
return this.fb.group(
{
email: [entity ? entity.email : '', [Validators.required, Validators.email]],
email: [entity ? entity.email : '', [Validators.required, validateEmail]],
firstName: [entity ? entity.firstName : ''],
lastName: [entity ? entity.lastName : ''],
phone: [entity ? entity.phone : ''],

2
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -43,7 +43,7 @@
</div>
<mat-form-field class="tb-appearance-transparent">
<mat-label translate>login.username</mat-label>
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
<input id="username-input" matInput type="email" autofocus formControlName="username"/>
<mat-icon matPrefix>email</mat-icon>
<mat-error *ngIf="loginFormGroup.get('username').invalid">
{{ 'login.invalid-email-format' | translate }}

7
ui-ngx/src/app/modules/login/pages/login/login.component.ts

@ -19,11 +19,12 @@ import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { Constants } from '@shared/models/constants';
import { Router } from '@angular/router';
import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models';
import { validateEmail } from '@app/core/utils';
@Component({
selector: 'tb-login',
@ -35,8 +36,8 @@ export class LoginComponent extends PageComponent implements OnInit {
passwordViolation = false;
loginFormGroup = this.fb.group({
username: '',
password: ''
username: ['', [Validators.required, validateEmail]],
password: ['']
});
oauth2Clients: Array<OAuth2ClientLoginInfo> = null;

2
ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html

@ -33,7 +33,7 @@
<span style="height: 50px;"></span>
<mat-form-field class="mat-block tb-appearance-transparent" hideRequiredMarker>
<mat-label translate>login.email</mat-label>
<input matInput type="email" autofocus formControlName="email" email required/>
<input matInput type="email" autofocus formControlName="email"/>
<mat-icon class="material-icons" matPrefix>email</mat-icon>
<mat-error *ngIf="requestPasswordRequest.get('email').invalid">
{{ 'user.invalid-email-format' | translate }}

3
ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts

@ -22,6 +22,7 @@ import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { validateEmail } from '@app/core/utils';
@Component({
selector: 'tb-reset-password-request',
@ -33,7 +34,7 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn
clicked: boolean = false;
requestPasswordRequest = this.fb.group({
email: ['', [Validators.email, Validators.required]]
email: ['', [Validators.required, validateEmail]],
}, {updateOn: 'submit'});
constructor(protected store: Store<AppState>,

2
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html

@ -47,7 +47,7 @@
(optionSelected)="selected($event)"
[displayWith]="displayEntitySubtypeFn">
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype">
<span [innerHTML]="entitySubtype | highlight:searchText"></span>
<span [innerHTML]="entitySubtype | customTranslate | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<div matSuffix>

12
ui-ngx/src/app/shared/components/file-input.component.html

@ -39,11 +39,13 @@
[flow]="flow.flowJs">
<div class="upload-label">
<mat-icon class="tb-mat-32 drop-label-icon">cloud_upload</mat-icon>
<span class="drop-label-text">{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file" [class]="uploadButtonClass">
<label
for="{{inputId}}">{{ uploadButtonText ? uploadButtonText : ((multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate) }}</label>
</button>
<div>
<span class="drop-label-text">{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file" [class]="uploadButtonClass">
<label
for="{{inputId}}">{{ uploadButtonText ? uploadButtonText : ((multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate) }}</label>
</button>
</div>
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs"
[flowAttributes]="{accept: accept}" id="{{inputId}}">
</div>

2
ui-ngx/src/app/shared/components/phone-input.component.html

@ -27,7 +27,7 @@
</mat-option>
</mat-select>
</div>
<mat-form-field class="phone-input" [appearance]="appearance" [floatLabel]="floatLabel">
<mat-form-field class="phone-input" [appearance]="appearance" [floatLabel]="floatLabel" subscriptSizing="dynamic">
<mat-label>{{ label }}</mat-label>
<input
formControlName="phoneNumber"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save