Browse Source

Merge remote-tracking branch 'upstream/develop/3.5' into feature/notification-system

pull/7911/head
Vladyslav_Prykhodko 3 years ago
parent
commit
069ef4de5b
  1. 20
      application/pom.xml
  2. 10
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  3. 4
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  4. 32
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  5. 17
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  6. 19
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  7. 19
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  8. 137
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  9. 85
      application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java
  10. 4
      application/src/main/resources/thingsboard.yml
  11. 4
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  12. 52
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  13. 22
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  14. 17
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  15. 12
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  16. 2
      application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java
  17. 33
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  18. 58
      application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
  19. 1
      application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java
  20. 2
      application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java
  21. 20
      application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java
  22. 2
      application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java
  23. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java
  24. 64
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  25. 173
      application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java
  26. 4
      application/src/test/resources/application-test.properties
  27. 4
      application/src/test/resources/logback-test.xml
  28. 4
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  29. 12
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  30. 10
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  31. 5
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  32. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java
  33. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  34. 25
      dao/pom.xml
  35. 2
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  36. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  37. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  38. 9
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  39. 2
      dao/src/main/resources/cassandra/schema-ts-latest.cql
  40. 4
      dao/src/main/resources/cassandra/schema-ts.cql
  41. 366
      dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java
  42. 86
      dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java
  43. 760
      dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java
  44. 96
      dao/src/test/java/org/thingsboard/server/dao/AbstractNoSqlContainer.java
  45. 51
      dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java
  46. 88
      dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
  47. 16
      dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java
  48. 24
      dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java
  49. 4
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java
  50. 360
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  51. 3
      dao/src/test/resources/application-test.properties
  52. 2
      dao/src/test/resources/cassandra-test.properties
  53. 4
      dao/src/test/resources/logback.xml
  54. 2
      dao/src/test/resources/nosql-test.properties
  55. 2
      dao/src/test/resources/sql-test.properties
  56. 18
      msa/black-box-tests/README.md
  57. 1
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  58. 5
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
  59. 3
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  60. 4
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java
  61. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java
  62. 23
      msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml
  63. 19
      msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml
  64. 2
      msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml
  65. 7
      msa/black-box-tests/src/test/resources/docker-selenium.yml
  66. 5
      msa/pom.xml
  67. 31
      pom.xml
  68. 16
      rule-engine/rule-engine-components/pom.xml
  69. 41
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java
  70. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  71. 73
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java
  72. 0
      rule-engine/rule-engine-components/src/test/resources/pem/empty.pem
  73. 103
      rule-engine/rule-engine-components/src/test/resources/pem/tb-cloud-chain.pem
  74. 32
      rule-engine/rule-engine-components/src/test/resources/pem/tb-cloud.pem
  75. 16
      ui-ngx/.browserslistrc
  76. 3
      ui-ngx/angular.json
  77. 71
      ui-ngx/package.json
  78. 11
      ui-ngx/src/app/core/translate/translate-default-compiler.ts
  79. 2
      ui-ngx/src/app/core/utils.ts
  80. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  81. 23
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss
  82. 5
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html
  83. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  84. 7
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  85. 6
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss
  86. 7
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html
  87. 2
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss
  88. 2
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  89. 2
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html
  90. 13
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.scss
  91. 14
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  92. 23
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  93. 2
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html
  94. 2
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html
  95. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  96. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html
  97. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.scss
  98. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  99. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  100. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

20
application/pom.xml

@ -144,21 +144,6 @@
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
</dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>ui-ngx</artifactId>
@ -329,6 +314,11 @@
<artifactId>spring-test-dbunit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cassandra</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>

10
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -130,8 +130,8 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
@ -149,8 +149,8 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendUpdate()\"\n [class.mat-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendUpdate()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
@ -197,4 +197,4 @@
}
}
]
}
}

4
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

32
application/src/main/data/json/system/widget_bundles/input_widgets.json

File diff suppressed because one or more lines are too long

17
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@ -40,11 +42,15 @@ import java.util.Map;
@Configuration
@TbCoreComponent
@EnableWebSocket
@RequiredArgsConstructor
@Slf4j
public class WebSocketConfiguration implements WebSocketConfigurer {
public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/";
private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**";
private final WebSocketHandler wsHandler;
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
@ -55,7 +61,11 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*")
if (!(wsHandler instanceof TbWebSocketHandler)) {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
}
registry.addHandler(wsHandler, WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override
@ -82,11 +92,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
});
}
@Bean
public WebSocketHandler wsHandler() {
return new TbWebSocketHandler();
}
protected SecurityUser getCurrentUser() throws ThingsboardException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {

19
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -20,6 +20,7 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -41,11 +42,13 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -62,6 +65,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@RestController
@TbCoreComponent
@ -69,6 +74,10 @@ import java.net.URISyntaxException;
@Slf4j
@RequiredArgsConstructor
public class AuthController extends BaseController {
@Value("${server.rest.rate_limits.reset_password_per_user:5:3600}")
private String defaultLimitsConfiguration;
private final ConcurrentMap<UserId, TbRateLimits> resetPasswordRateLimits = new ConcurrentHashMap<>();
private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory;
private final MailService mailService;
@ -211,7 +220,12 @@ public class AuthController extends BaseController {
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId());
if (!tbRateLimits.tryConsume()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
try {
URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location);
@ -323,4 +337,9 @@ public class AuthController extends BaseController {
throw handleException(e);
}
}
private TbRateLimits getTbRateLimits(UserId userId) {
return resetPasswordRateLimits.computeIfAbsent(userId,
key -> new TbRateLimits(defaultLimitsConfiguration, true));
}
}

19
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -70,8 +70,8 @@ import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_O
@Slf4j
public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint {
private static final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired @Lazy
@ -85,13 +85,18 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
@Value("${server.ws.ping_timeout:30000}")
private long pingTimeout;
<<<<<<< HEAD
private ConcurrentMap<String, WebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
private ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
=======
private final ConcurrentMap<String, TelemetryWebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
>>>>>>> upstream/develop/3.5
private final ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {

137
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.ws;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
@ -27,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.DataConstants;
@ -98,6 +98,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -117,7 +119,6 @@ public class DefaultWebSocketService implements WebSocketService {
private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
private static final String PROCESSING_MSG = "[{}] Processing: {}";
private static final ObjectMapper jsonMapper = new ObjectMapper();
private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
@ -217,6 +218,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
try {
<<<<<<< HEAD:application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
switch (sessionRef.getSessionType()) {
case TELEMETRY:
processTelemetryCmds(sessionRef, msg);
@ -224,6 +226,45 @@ public class DefaultWebSocketService implements WebSocketService {
case NOTIFICATIONS:
processNotificationCmds(sessionRef, msg);
break;
=======
TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.OBJECT_MAPPER.readValue(msg, TelemetryPluginCmdsWrapper.class);
if (cmdsWrapper != null) {
if (cmdsWrapper.getAttrSubCmds() != null) {
cmdsWrapper.getAttrSubCmds().forEach(cmd -> {
if (processSubscription(sessionRef, cmd)) {
handleWsAttributesSubscriptionCmd(sessionRef, cmd);
}
});
}
if (cmdsWrapper.getTsSubCmds() != null) {
cmdsWrapper.getTsSubCmds().forEach(cmd -> {
if (processSubscription(sessionRef, cmd)) {
handleWsTimeseriesSubscriptionCmd(sessionRef, cmd);
}
});
}
if (cmdsWrapper.getHistoryCmds() != null) {
cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityDataCmds() != null) {
cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd));
}
if (cmdsWrapper.getAlarmDataCmds() != null) {
cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountCmds() != null) {
cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) {
cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) {
cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
>>>>>>> upstream/develop/3.5:application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@ -468,7 +509,6 @@ public class DefaultWebSocketService implements WebSocketService {
@Override
public void onSuccess(List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, 0L));
@ -476,6 +516,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope());
Lock subLock = new ReentrantLock();
TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
@ -485,9 +526,28 @@ public class DefaultWebSocketService implements WebSocketService {
.allKeys(false)
.keyStates(subState)
.scope(scope)
<<<<<<< HEAD:application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
.updateProcessor((subscription, update) -> sendWsMsg(subscription.getSessionId(), update))
=======
.updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
>>>>>>> upstream/develop/3.5:application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
.build();
oldSubService.addSubscription(sub);
subLock.lock();
try{
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
}
@Override
@ -568,13 +628,13 @@ public class DefaultWebSocketService implements WebSocketService {
@Override
public void onSuccess(List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope());
Lock subLock = new ReentrantLock();
TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
@ -583,9 +643,30 @@ public class DefaultWebSocketService implements WebSocketService {
.entityId(entityId)
.allKeys(true)
.keyStates(subState)
<<<<<<< HEAD:application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
.updateProcessor((subscription, update) -> sendWsMsg(subscription.getSessionId(), update))
.scope(scope).build();
oldSubService.addSubscription(sub);
=======
.updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
.scope(scope)
.build();
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
>>>>>>> upstream/develop/3.5:application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
}
@Override
@ -658,20 +739,38 @@ public class DefaultWebSocketService implements WebSocketService {
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size());
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
.subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
<<<<<<< HEAD:application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
.updateProcessor((subscription, update) -> sendWsMsg(subscription.getSessionId(), update))
=======
.updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
>>>>>>> upstream/develop/3.5:application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
.allKeys(true)
.keyStates(subState).build();
oldSubService.addSubscription(sub);
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
}
@Override
@ -695,21 +794,39 @@ public class DefaultWebSocketService implements WebSocketService {
return new FutureCallback<>() {
@Override
public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
.subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
<<<<<<< HEAD:application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
.updateProcessor((subscription, update) -> sendWsMsg(subscription.getSessionId(), update))
=======
.updateConsumer((sessionId, update) -> {
subLock.lock();
try {
sendWsMsg(sessionId, update);
} finally {
subLock.unlock();
}
})
>>>>>>> upstream/develop/3.5:application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
.allKeys(false)
.keyStates(subState).build();
oldSubService.addSubscription(sub);
subLock.lock();
try{
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
}
@Override
@ -815,7 +932,7 @@ public class DefaultWebSocketService implements WebSocketService {
private void sendWsMsg(WebSocketSessionRef sessionRef, int cmdId, Object update) {
try {
String msg = jsonMapper.writeValueAsString(update);
String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update);
executor.submit(() -> {
try {
msgEndpoint.send(sessionRef, cmdId, msg);

85
application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java

@ -1,85 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* This class deduplicate executions of the specified function.
* Useful in cluster mode, when you get event about partition change multiple times.
* Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
* later, once the processing of first event is done, process last pending task.
*
* @param <P> parameters of the function
*/
@Slf4j
public class EventDeduplicationExecutor<P> {
private final String name;
private final ExecutorService executor;
private final Consumer<P> function;
private P pendingTask;
private boolean busy;
public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer<P> function) {
this.name = name;
this.executor = executor;
this.function = function;
}
public void submit(P params) {
log.info("[{}] Going to submit: {}", name, params);
synchronized (EventDeduplicationExecutor.this) {
if (!busy) {
busy = true;
pendingTask = null;
try {
log.info("[{}] Submitting task: {}", name, params);
executor.submit(() -> {
try {
log.info("[{}] Executing task: {}", name, params);
function.accept(params);
} catch (Throwable e) {
log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
throw e;
} finally {
unlockAndProcessIfAny();
}
});
} catch (Throwable e) {
log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
unlockAndProcessIfAny();
throw e;
}
} else {
log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
pendingTask = params;
}
}
}
private void unlockAndProcessIfAny() {
synchronized (EventDeduplicationExecutor.this) {
busy = false;
if (pendingTask != null) {
submit(pendingTask);
}
}
}
}

4
application/src/main/resources/thingsboard.yml

@ -73,6 +73,8 @@ server:
min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
# Default value of the server side RPC timeout.
default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
rate_limits:
reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}"
# Application info
app:
@ -583,6 +585,7 @@ spring:
password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
hikari:
maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
registerMbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" # true - enable MBean to diagnose pools state via JMX
# Audit log parameters
audit-log:
@ -1224,3 +1227,4 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'

4
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -52,8 +52,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
@LocalServerPort
protected int wsPort;
<<<<<<< HEAD
private TbTestWebSocketClient wsClient; // lazy
private TbTestWebSocketClient anotherWsClient; // lazy
=======
private volatile TbTestWebSocketClient wsClient; // lazy
>>>>>>> upstream/develop/3.5
public TbTestWebSocketClient getWsClient() {
if (wsClient == null) {

52
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -29,14 +29,19 @@ import lombok.extern.slf4j.Slf4j;
import org.hamcrest.Matcher;
import org.hibernate.exception.ConstraintViolationException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@ -52,6 +57,9 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -71,6 +79,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -87,7 +96,6 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.mail.TestMailService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@ -102,6 +110,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@ -149,6 +158,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected MockMvc mockMvc;
protected String currentActivateToken;
protected String currentResetPasswordToken;
protected String token;
protected String refreshToken;
protected String username;
@ -173,6 +185,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
private TenantProfileService tenantProfileService;
@SpyBean
protected MailService mailService;
@Rule
public TestRule watcher = new TestWatcher() {
protected void starting(Description description) {
@ -203,7 +218,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Before
public void setupWebTest() throws Exception {
log.info("Executing web test setup");
log.debug("Executing web test setup");
setupMailServiceMock();
if (this.mockMvc == null) {
this.mockMvc = webAppContextSetup(webApplicationContext)
@ -243,12 +260,33 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
resetTokens();
log.info("Executed web test setup");
log.debug("Executed web test setup");
}
private void setupMailServiceMock() throws ThingsboardException {
Mockito.doNothing().when(mailService).sendAccountActivatedEmail(anyString(), anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String activationLink = (String) args[0];
currentActivateToken = activationLink.split("=")[1];
return null;
}
}).when(mailService).sendActivationEmail(anyString(), anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String passwordResetLink = (String) args[0];
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmailAsync(anyString(), anyString());
}
@After
public void teardownWebTest() throws Exception {
log.info("Executing web test teardown");
log.debug("Executing web test teardown");
loginSysAdmin();
doDelete("/api/tenant/" + tenantId.getId().toString())
@ -376,11 +414,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
private JsonNode getActivateRequest(String password) throws Exception {
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
doGet("/api/noauth/activate?activateToken={activateToken}", this.currentActivateToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + this.currentActivateToken));
return new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken)
.put("activateToken", this.currentActivateToken)
.put("password", password);
}

22
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -21,12 +21,9 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.mail.DefaultMailService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@ -35,6 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -43,12 +42,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public abstract class BaseAdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
@Autowired
MailService mailService;
@Autowired
DefaultMailService defaultMailService;
@Test
public void testFindAdminSettingsByKey() throws Exception {
loginSysAdmin();
@ -118,10 +111,12 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
@Test
public void testSendTestMail() throws Exception {
Mockito.doNothing().when(mailService).sendTestMail(any(), anyString());
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
doPost("/api/admin/settings/testMail", adminSettings)
.andExpect(status().isOk());
Mockito.verify(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
}
@Test
@ -137,15 +132,8 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
adminSettings.setJsonValue(objectNode);
Mockito.doAnswer((invocations) -> {
var jsonConfig = (JsonNode) invocations.getArgument(0);
var email = (String) invocations.getArgument(1);
defaultMailService.sendTestMail(jsonConfig, email);
return null;
}).when(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any());
Mockito.verify(mailService).sendTestMail(Mockito.any(), Mockito.anyString());
}
void resetJwtSettingsToDefault() throws Exception {

17
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.user.UserDao;
import org.thingsboard.server.service.mail.TestMailService;
import java.util.ArrayList;
import java.util.Collections;
@ -54,6 +53,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -106,12 +107,12 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
resetTokens();
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
doGet("/api/noauth/activate?activateToken={activateToken}", this.currentActivateToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + this.currentActivateToken));
JsonNode activateRequest = new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken)
.put("activateToken", this.currentActivateToken)
.put("password", "testPassword");
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
@ -208,17 +209,19 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk());
Thread.sleep(1000);
doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + this.currentResetPasswordToken));
JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
.put("resetToken", TestMailService.currentResetPasswordToken)
.put("resetToken", this.currentResetPasswordToken)
.put("password", "testPassword2");
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
JsonNode tokenInfo = readResponse(
doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isOk()), JsonNode.class);
Mockito.verify(mailService).sendPasswordWasResetEmail(anyString(), anyString());
validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user")

12
application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java

@ -548,7 +548,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
SingleEntityFilter entityFilter = new SingleEntityFilter();
entityFilter.setSingleEntity(tenantId);
assertThatNoException().isThrownBy(() -> {
assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> {
JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr"));
assertThat(update.get("errorMsg").isNull()).isTrue();
assertThat(update.get("errorCode").asInt()).isEqualTo(SubscriptionErrorCode.NO_ERROR.getCode());
@ -560,7 +560,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("attr", expectedAttrValue))
));
JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate());
assertThat(update).isNotNull();
assertThat(update).as("waitForUpdate").isNotNull();
assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue);
}
@ -569,15 +569,17 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
log.debug("sendTelemetry callback onSuccess");
latch.countDown();
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to send telemetry", t);
latch.countDown();
}
});
latch.await(3, TimeUnit.SECONDS);
assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendTelemetry callback");
}
private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List<AttributeKvEntry> attrData) throws InterruptedException {
@ -589,14 +591,16 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
tsService.saveAndNotify(tenantId, entityId, scope.name(), attrData, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
log.debug("sendAttributes callback onSuccess");
latch.countDown();
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to sendAttributes", t);
latch.countDown();
}
});
latch.await(3, TimeUnit.SECONDS);
assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendAttributes callback").isTrue();
}
}

2
application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java

@ -192,7 +192,7 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController
WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
savedWidgetsBundle.setAlias("new_alias");
Mockito.reset(tbClusterService);
Mockito.clearInvocations(tbClusterService);
doPost("/api/widgetsBundle", savedWidgetsBundle)
.andExpect(status().isBadRequest())

33
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -48,7 +48,11 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class TbTestWebSocketClient extends WebSocketClient {
<<<<<<< HEAD
@Getter
=======
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30);
>>>>>>> upstream/develop/3.5
private volatile String lastMsg;
private volatile CountDownLatch reply;
private volatile CountDownLatch update;
@ -89,13 +93,18 @@ public class TbTestWebSocketClient extends WebSocketClient {
}
public void registerWaitForUpdate(int count) {
log.debug("registerWaitForUpdate [{}]", count);
lastMsg = null;
update = new CountDownLatch(count);
}
@Override
public void send(String text) throws NotYetConnectedException {
<<<<<<< HEAD
log.info("SENDING: {}", text);
=======
log.debug("send [{}]", text);
>>>>>>> upstream/develop/3.5
reply = new CountDownLatch(1);
super.send(text);
}
@ -113,6 +122,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
}
public String waitForUpdate() {
<<<<<<< HEAD
return waitForUpdate(false);
}
@ -128,9 +138,19 @@ public class TbTestWebSocketClient extends WebSocketClient {
try {
if (update.await(ms, TimeUnit.MILLISECONDS)) {
return lastMsg;
=======
return waitForUpdate(TIMEOUT);
}
public String waitForUpdate(long ms) {
log.debug("waitForUpdate [{}]", ms);
try {
if (!update.await(ms, TimeUnit.MILLISECONDS)) {
log.warn("Failed to await update (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace"));
>>>>>>> upstream/develop/3.5
}
} catch (InterruptedException e) {
log.warn("Failed to await reply", e);
log.warn("Failed to await update", e);
}
if (throwExceptionOnTimeout) {
throw new AssertionError("Waited for update for " + ms + " ms but none arrived");
@ -140,6 +160,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
}
public String waitForReply() {
<<<<<<< HEAD
return waitForReply(false);
}
@ -147,6 +168,16 @@ public class TbTestWebSocketClient extends WebSocketClient {
try {
if (reply.await(3, TimeUnit.SECONDS)) {
return lastMsg;
=======
return waitForReply(TIMEOUT);
}
public String waitForReply(long ms) {
log.debug("waitForReply [{}]", ms);
try {
if (!reply.await(ms, TimeUnit.MILLISECONDS)) {
log.warn("Failed to await reply (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace"));
>>>>>>> upstream/develop/3.5
}
} catch (InterruptedException e) {
log.warn("Failed to await reply", e);

58
application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java

@ -1,58 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@Profile("test")
@Configuration
public class TestMailService {
public static String currentActivateToken;
public static String currentResetPasswordToken;
@Bean
@Primary
public MailService mailService() throws ThingsboardException {
MailService mailService = Mockito.mock(MailService.class);
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String activationLink = (String) args[0];
currentActivateToken = activationLink.split("=")[1];
return null;
}
}).when(mailService).sendActivationEmail(Mockito.anyString(), Mockito.anyString());
Mockito.doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
String passwordResetLink = (String) args[0];
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmailAsync(Mockito.anyString(), Mockito.anyString());
return mailService;
}
}

1
application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java

@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"js.evaluator=local",
"js.max_script_body_size=50",
"js.max_total_args_size=50",
"js.max_result_size=50",

2
application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java

@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class AbstractTransportIntegrationTest extends AbstractControllerTest {
protected static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 10;
protected static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
protected static final String MQTT_URL = "tcp://localhost:1883";
protected static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";

20
application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java

@ -15,30 +15,14 @@
*/
package org.thingsboard.server.transport;
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.dao.CustomCassandraCQLUnit;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import java.util.Arrays;
import org.thingsboard.server.dao.AbstractNoSqlContainer;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.transport.*.telemetry.timeseries.nosql.*Test",
})
public class TransportNoSqlTestSuite {
@ClassRule
public static CustomCassandraCQLUnit cassandraUnit =
new CustomCassandraCQLUnit(
Arrays.asList(
new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false)
),
"cassandra-test.yaml", 30000l);
public class TransportNoSqlTestSuite extends AbstractNoSqlContainer {
}

2
application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java

@ -45,6 +45,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@TestPropertySource(properties = {
"coap.enabled=true",
"service.integrations.supported=ALL",
"transport.coap.enabled=true",
})
@Slf4j

2
application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java

@ -46,8 +46,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@TestPropertySource(properties = {
"service.integrations.supported=ALL",
"transport.mqtt.enabled=true",
"js.evaluator=mock",
})
@Slf4j
public abstract class AbstractMqttIntegrationTest extends AbstractTransportIntegrationTest {

64
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.mqtt.mqttv3.attributes;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.os72.protobuf.dynamic.DynamicSchema;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
@ -22,6 +23,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.squareup.wire.schema.internal.parser.ProtoFileElement;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DynamicProtoUtils;
@ -45,6 +47,7 @@ import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -125,14 +128,16 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateUpdateAttributesJsonResponse(onUpdateCallback, SHARED_ATTRIBUTES_PAYLOAD);
MqttTestCallback onDeleteCallback = new MqttTestCallback();
client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE);
client.disconnect();
}
@ -145,13 +150,15 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateUpdateAttributesProtoResponse(onUpdateCallback);
MqttTestCallback onDeleteCallback = new MqttTestCallback();
client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateDeleteAttributesProtoResponse(onDeleteCallback);
client.disconnect();
}
@ -162,7 +169,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateUpdateAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared");
attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList);
@ -178,7 +185,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateDeleteAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson");
@ -209,7 +216,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onUpdateCallback").isTrue();
validateJsonGatewayUpdateAttributesResponse(onUpdateCallback, deviceName, SHARED_ATTRIBUTES_PAYLOAD);
@ -217,7 +225,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.setCallback(onDeleteCallback);
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class);
onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await onDeleteCallback").isTrue();
validateJsonGatewayUpdateAttributesResponse(onDeleteCallback, deviceName, SHARED_ATTRIBUTES_DELETED_RESPONSE);
client.disconnect();
@ -246,7 +255,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateJsonGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName, String expectResultData) {
assertNotNull(callback.getPayloadBytes());
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
assertEquals(JacksonUtil.toJsonNode(getGatewayAttributesResponseJson(deviceName, expectResultData)), JacksonUtil.fromBytes(callback.getPayloadBytes()));
}
@ -260,8 +269,9 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateProtoGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertNotNull(callback.getPayloadBytes());
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList("shared");
@ -285,8 +295,9 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateProtoGatewayDeleteAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertNotNull(callback.getPayloadBytes());
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertThat(callback.getPayloadBytes()).as("callback payload non-null").isNotNull();
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson");
TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
@ -391,9 +402,19 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
100);
assertNotNull(device);
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String attributeValuesUrl = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/CLIENT_SCOPE?keys=" + clientKeysStr;
Awaitility.await()
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {});
return attributes.size() == 5;
});
SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(device.getId());
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(","));
List<String> sharedKeysList = List.of(sharedKeysStr.split(","));
@ -538,13 +559,15 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateJsonResponse(MqttTestCallback callback, String expectedResponse) throws InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes()));
}
protected void validateProtoResponse(MqttTestCallback callback, TransportProtos.GetAttributeResponseMsg expectedResponse) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
assertEquals(expectedResponse.getRequestId(), actualAttributesResponse.getRequestId());
@ -567,14 +590,16 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateJsonResponseGateway(MqttTestCallback callback, String deviceName, String expectedValues) throws InterruptedException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
String expectedRequestPayload = "{\"id\":1,\"device\":\"" + deviceName + "\",\"values\":" + expectedValues + "}";
assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes()));
}
protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true);
TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
@ -590,7 +615,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}
protected void validateProtoSharedResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, false);
TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());

173
application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java

@ -1,173 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.util;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.utils.EventDeduplicationExecutor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class EventDeduplicationExecutorTest {
ThingsBoardThreadFactory threadFactory = ThingsBoardThreadFactory.forName(getClass().getSimpleName());
ExecutorService executor;
@After
public void tearDown() throws Exception {
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void testSimpleFlowSameThread() throws InterruptedException {
simpleFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testPeriodicFlowSameThread() throws InterruptedException {
periodicFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testExceptionFlowSameThread() throws InterruptedException {
exceptionFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testSimpleFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
simpleFlow(executor);
}
@Test
public void testPeriodicFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
periodicFlow(executor);
}
@Test
public void testExceptionFlowSingleThread() throws InterruptedException {
executor = Executors.newSingleThreadExecutor(threadFactory);
exceptionFlow(executor);
}
@Test
public void testSimpleFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
simpleFlow(executor);
}
@Test
public void testPeriodicFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
periodicFlow(executor);
}
@Test
public void testExceptionFlowMultiThread() throws InterruptedException {
executor = Executors.newFixedThreadPool(3, threadFactory);
exceptionFlow(executor);
}
private void simpleFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
executor.submit(params2);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void periodicFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
Thread.sleep(500);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void exceptionFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
Mockito.doThrow(new RuntimeException()).when(function).accept("params1");
executor.submit(params1);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
public static class StringConsumer implements Consumer<String> {
@Override
public void accept(String s) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

4
application/src/test/resources/application-test.properties

@ -1,3 +1,4 @@
js.evaluator=mock
transport.lwm2m.server.security.credentials.enabled=true
transport.lwm2m.server.security.credentials.type=KEYSTORE
transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks
@ -20,6 +21,9 @@ transport.mqtt.enabled=false
transport.coap.enabled=false
transport.lwm2m.enabled=false
transport.snmp.enabled=false
coap.enabled=false
integrations.rpc.enabled=false
service.integrations.supported=NONE
# Low latency settings to perform tests as fast as possible
sql.attributes.batch_max_delay=5

4
application/src/test/resources/logback-test.xml

@ -13,8 +13,10 @@
<logger name="org.springframework" level="WARN"/>
<logger name="org.springframework.boot.test" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="INFO"/>
<logger name="org.testcontainers" level="INFO" />
<logger name="org.eclipse.leshan" level="INFO"/>
<logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>

4
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java

@ -108,6 +108,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Override
public void evict(Collection<K> keys) {
//Redis expects at least 1 key to delete. Otherwise - ERR wrong number of arguments for 'del' command
if (keys.isEmpty()) {
return;
}
try (var connection = connectionFactory.getConnection()) {
connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
}

12
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -18,9 +18,14 @@ package org.thingsboard.server.common.data;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom;
import java.util.Base64;
import static org.apache.commons.lang3.StringUtils.repeat;
public class StringUtils {
public static final SecureRandom RANDOM = new SecureRandom();
public static final String EMPTY = "";
public static final int INDEX_NOT_FOUND = -1;
@ -189,4 +194,11 @@ public class StringUtils {
return RandomStringUtils.randomAlphabetic(count);
}
public static String generateSafeToken(int length) {
byte[] bytes = new byte[length];
RANDOM.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(bytes);
}
}

10
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -68,7 +68,7 @@ public class HashPartitionService implements PartitionService {
private final TenantRoutingInfoService tenantRoutingInfoService;
private final QueueRoutingInfoService queueRoutingInfoService;
private ConcurrentMap<QueueKey, List<Integer>> myPartitions = new ConcurrentHashMap<>();
private volatile ConcurrentMap<QueueKey, List<Integer>> myPartitions = new ConcurrentHashMap<>();
private final ConcurrentMap<QueueKey, String> partitionTopicsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<QueueKey, Integer> partitionSizesMap = new ConcurrentHashMap<>();
@ -217,17 +217,19 @@ public class HashPartitionService implements PartitionService {
}
queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId)));
ConcurrentMap<QueueKey, List<Integer>> oldPartitions = myPartitions;
myPartitions = new ConcurrentHashMap<>();
final ConcurrentMap<QueueKey, List<Integer>> newPartitions = new ConcurrentHashMap<>();
partitionSizesMap.forEach((queueKey, size) -> {
for (int i = 0; i < size; i++) {
ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i);
if (currentService.equals(serviceInfo)) {
myPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i);
newPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i);
}
}
});
final ConcurrentMap<QueueKey, List<Integer>> oldPartitions = myPartitions;
myPartitions = newPartitions;
oldPartitions.forEach((queueKey, partitions) -> {
if (!myPartitions.containsKey(queueKey)) {
log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey);

5
common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -109,17 +109,20 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
private final Lock scriptsLock = new ReentrantLock();
@PostConstruct
@Override
public void init() {
super.init();
requestTemplate.init();
}
@PreDestroy
public void destroy() {
@Override
public void stop() {
super.stop();
if (requestTemplate != null) {
requestTemplate.stop();
}
callbackExecutor.shutdownNow();
}
@Override

5
common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java

@ -104,6 +104,7 @@ public class NashornJsInvokeService extends AbstractJsInvokeService {
}
@PostConstruct
@Override
public void init() {
super.init();
jsExecutor = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(jsExecutorThreadPoolSize));
@ -122,11 +123,15 @@ public class NashornJsInvokeService extends AbstractJsInvokeService {
}
@PreDestroy
@Override
public void stop() {
super.stop();
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
if (jsExecutor != null) {
jsExecutor.shutdownNow();
}
}
@Override

5
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -119,6 +119,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
@SneakyThrows
@PostConstruct
@Override
public void init() {
super.init();
OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE);
@ -142,7 +143,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
}
@PreDestroy
public void destroy() {
@Override
public void stop() {
super.stop();
if (executor != null) {
executor.shutdownNow();
}

25
dao/pom.xml

@ -162,26 +162,6 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-thrift</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
@ -211,6 +191,11 @@
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cassandra</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>

2
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -66,7 +66,7 @@ public class CachedAttributesService implements AttributesService {
private final TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache;
private ListeningExecutorService cacheExecutor;
@Value("${cache.type}")
@Value("${cache.type:caffeine}")
private String cacheType;
public CachedAttributesService(AttributesDao attributesDao,

10
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -805,6 +805,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) {
ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter());
if (filter.getEntityNameFilter().startsWith("%") || filter.getEntityNameFilter().endsWith("%")) {
return "lower(e.search_text) like lower(:entity_filter_name_filter)";
}
return "lower(e.search_text) like lower(concat(:entity_filter_name_filter, '%%'))";
}
@ -833,6 +838,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
}
ctx.addStringParameter("entity_filter_type_query_type", type);
ctx.addStringParameter("entity_filter_type_query_name", name);
if (name.startsWith("%") || name.endsWith("%")) {
return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(:entity_filter_type_query_name)";
}
return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))";
}

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -228,7 +228,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
long ts = latest.getTs();
ListenableFuture<Boolean> removedLatestFuture;
if (ts > query.getStartTs() && ts <= query.getEndTs()) {
if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));

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

@ -29,7 +29,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -40,7 +39,6 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -51,6 +49,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
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;
@ -126,7 +125,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (user.getId() == null) {
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials);
}
@ -192,7 +191,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (!userCredentials.isEnabled()) {
throw new DisabledException(String.format("User credentials not enabled [%s]", email));
}
userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
}
@ -202,7 +201,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user");
}
userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
}

2
dao/src/main/resources/cassandra/schema-ts-latest.cql

@ -15,7 +15,7 @@
--
CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf (
entity_type text, // (DEVICE, CUSTOMER, TENANT)
entity_type text, -- (DEVICE, CUSTOMER, TENANT)
entity_id timeuuid,
key text,
ts bigint,

4
dao/src/main/resources/cassandra/schema-ts.cql

@ -15,7 +15,7 @@
--
CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf (
entity_type text, // (DEVICE, CUSTOMER, TENANT)
entity_type text, -- (DEVICE, CUSTOMER, TENANT)
entity_id timeuuid,
key text,
partition bigint,
@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf (
);
CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf (
entity_type text, // (DEVICE, CUSTOMER, TENANT)
entity_type text, -- (DEVICE, CUSTOMER, TENANT)
entity_id timeuuid,
key text,
partition bigint,

366
dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java

@ -1,366 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.apache.cassandra.io.sstable;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Objects;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.io.sstable.format.Version;
import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer;
import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer;
import org.apache.cassandra.io.sstable.metadata.MetadataSerializer;
import org.apache.cassandra.utils.Pair;
import static org.apache.cassandra.io.sstable.Component.separator;
/**
* A SSTable is described by the keyspace and column family it contains data
* for, a generation (where higher generations contain more recent data) and
* an alphabetic version string.
*
* A descriptor can be marked as temporary, which influences generated filenames.
*/
public class Descriptor
{
public static String TMP_EXT = ".tmp";
/** canonicalized path to the directory where SSTable resides */
public final File directory;
/** version has the following format: <code>[a-z]+</code> */
public final Version version;
public final String ksname;
public final String cfname;
public final int generation;
public final SSTableFormat.Type formatType;
/** digest component - might be {@code null} for old, legacy sstables */
public final Component digestComponent;
private final int hashCode;
/**
* A descriptor that assumes CURRENT_VERSION.
*/
@VisibleForTesting
public Descriptor(File directory, String ksname, String cfname, int generation)
{
this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null);
}
/**
* Constructor for sstable writers only.
*/
public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
}
@VisibleForTesting
public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
}
public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent)
{
assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass());
this.version = version;
try
{
this.directory = directory.getCanonicalFile();
}
catch (IOException e)
{
throw new IOError(e);
}
this.ksname = ksname;
this.cfname = cfname;
this.generation = generation;
this.formatType = formatType;
this.digestComponent = digestComponent;
hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType);
}
public Descriptor withGeneration(int newGeneration)
{
return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType, digestComponent);
}
public Descriptor withFormatType(SSTableFormat.Type newType)
{
return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType, digestComponent);
}
public Descriptor withDigestComponent(Component newDigestComponent)
{
return new Descriptor(version, directory, ksname, cfname, generation, formatType, newDigestComponent);
}
public String tmpFilenameFor(Component component)
{
return filenameFor(component) + TMP_EXT;
}
public String filenameFor(Component component)
{
return baseFilename() + separator + component.name();
}
public String baseFilename()
{
StringBuilder buff = new StringBuilder();
buff.append(directory).append(File.separatorChar);
appendFileName(buff);
return buff.toString();
}
private void appendFileName(StringBuilder buff)
{
if (!version.hasNewFileName())
{
buff.append(ksname).append(separator);
buff.append(cfname).append(separator);
}
buff.append(version).append(separator);
buff.append(generation);
if (formatType != SSTableFormat.Type.LEGACY)
buff.append(separator).append(formatType.name);
}
public String relativeFilenameFor(Component component)
{
final StringBuilder buff = new StringBuilder();
appendFileName(buff);
buff.append(separator).append(component.name());
return buff.toString();
}
public SSTableFormat getFormat()
{
return formatType.info;
}
/** Return any temporary files found in the directory */
public List<File> getTemporaryFiles()
{
List<File> ret = new ArrayList<>();
File[] tmpFiles = directory.listFiles((dir, name) ->
name.endsWith(Descriptor.TMP_EXT));
for (File tmpFile : tmpFiles)
ret.add(tmpFile);
return ret;
}
/**
* Files obsoleted by CASSANDRA-7066 : temporary files and compactions_in_progress. We support
* versions 2.1 (ka) and 2.2 (la).
* Temporary files have tmp- or tmplink- at the beginning for 2.2 sstables or after ks-cf- for 2.1 sstables
*/
private final static String LEGACY_COMP_IN_PROG_REGEX_STR = "^compactions_in_progress(\\-[\\d,a-f]{32})?$";
private final static Pattern LEGACY_COMP_IN_PROG_REGEX = Pattern.compile(LEGACY_COMP_IN_PROG_REGEX_STR);
private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$";
private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR);
public static boolean isLegacyFile(File file)
{
if (file.isDirectory())
return file.getParentFile() != null &&
file.getParentFile().getName().equalsIgnoreCase("system") &&
LEGACY_COMP_IN_PROG_REGEX.matcher(file.getName()).matches();
else
return LEGACY_TMP_REGEX.matcher(file.getName()).matches();
}
public static boolean isValidFile(String fileName)
{
return fileName.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(fileName).matches();
}
/**
* @see #fromFilename(File directory, String name)
* @param filename The SSTable filename
* @return Descriptor of the SSTable initialized from filename
*/
public static Descriptor fromFilename(String filename)
{
return fromFilename(filename, false);
}
public static Descriptor fromFilename(String filename, SSTableFormat.Type formatType)
{
return fromFilename(filename).withFormatType(formatType);
}
public static Descriptor fromFilename(String filename, boolean skipComponent)
{
File file = new File(filename).getAbsoluteFile();
return fromFilename(file.getParentFile(), file.getName(), skipComponent).left;
}
public static Pair<Descriptor, String> fromFilename(File directory, String name)
{
return fromFilename(directory, name, false);
}
/**
* Filename of the form is vary by version:
*
* <ul>
* <li>&lt;ksname&gt;-&lt;cfname&gt;-(tmp-)?&lt;version&gt;-&lt;gen&gt;-&lt;component&gt; for cassandra 2.0 and before</li>
* <li>(&lt;tmp marker&gt;-)?&lt;version&gt;-&lt;gen&gt;-&lt;component&gt; for cassandra 3.0 and later</li>
* </ul>
*
* If this is for SSTable of secondary index, directory should ends with index name for 2.1+.
*
* @param directory The directory of the SSTable files
* @param name The name of the SSTable file
* @param skipComponent true if the name param should not be parsed for a component tag
*
* @return A Descriptor for the SSTable, and the Component remainder.
*/
@SuppressWarnings("deprecation")
public static Pair<Descriptor, String> fromFilename(File directory, String name, boolean skipComponent)
{
File parentDirectory = directory != null ? directory : new File(".");
// tokenize the filename
StringTokenizer st = new StringTokenizer(name, String.valueOf(separator));
String nexttok;
// read tokens backwards to determine version
Deque<String> tokenStack = new ArrayDeque<>();
while (st.hasMoreTokens())
{
tokenStack.push(st.nextToken());
}
// component suffix
String component = skipComponent ? null : tokenStack.pop();
nexttok = tokenStack.pop();
// generation OR format type
SSTableFormat.Type fmt = SSTableFormat.Type.LEGACY;
if (!CharMatcher.digit().matchesAllOf(nexttok))
{
fmt = SSTableFormat.Type.validate(nexttok);
nexttok = tokenStack.pop();
}
// generation
int generation = Integer.parseInt(nexttok);
// version
nexttok = tokenStack.pop();
if (!Version.validate(nexttok))
throw new UnsupportedOperationException("SSTable " + name + " is too old to open. Upgrade to 2.0 first, and run upgradesstables");
Version version = fmt.info.getVersion(nexttok);
// ks/cf names
String ksname, cfname;
if (version.hasNewFileName())
{
// for 2.1+ read ks and cf names from directory
File cfDirectory = parentDirectory;
// check if this is secondary index
String indexName = "";
if (cfDirectory.getName().startsWith(Directories.SECONDARY_INDEX_NAME_SEPARATOR))
{
indexName = cfDirectory.getName();
cfDirectory = cfDirectory.getParentFile();
}
if (cfDirectory.getName().equals(Directories.BACKUPS_SUBDIR))
{
cfDirectory = cfDirectory.getParentFile();
}
else if (cfDirectory.getParentFile().getName().equals(Directories.SNAPSHOT_SUBDIR))
{
cfDirectory = cfDirectory.getParentFile().getParentFile();
}
cfname = cfDirectory.getName().split("-")[0] + indexName;
ksname = cfDirectory.getParentFile().getName();
}
else
{
cfname = tokenStack.pop();
ksname = tokenStack.pop();
}
assert tokenStack.isEmpty() : "Invalid file name " + name + " in " + directory;
return Pair.create(new Descriptor(version, parentDirectory, ksname, cfname, generation, fmt,
// _assume_ version from version
Component.digestFor(version.uncompressedChecksumType())),
component);
}
@SuppressWarnings("deprecation")
public IMetadataSerializer getMetadataSerializer()
{
if (version.hasNewStatsFile())
return new MetadataSerializer();
else
return new LegacyMetadataSerializer();
}
/**
* @return true if the current Cassandra version can read the given sstable version
*/
public boolean isCompatible()
{
return version.isCompatible();
}
@Override
public String toString()
{
return baseFilename();
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof Descriptor))
return false;
Descriptor that = (Descriptor)o;
return that.directory.equals(this.directory)
&& that.generation == this.generation
&& that.ksname.equals(this.ksname)
&& that.cfname.equals(this.cfname)
&& that.formatType == this.formatType;
}
@Override
public int hashCode()
{
return hashCode;
}
}

86
dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java

@ -1,86 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.apache.cassandra.io.sstable.format;
import com.google.common.base.CharMatcher;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.RowIndexEntry;
import org.apache.cassandra.db.SerializationHeader;
import org.apache.cassandra.io.sstable.format.big.BigFormat;
/**
* Provides the accessors to data on disk.
*/
public interface SSTableFormat
{
static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment");
Version getLatestVersion();
Version getVersion(String version);
SSTableWriter.Factory getWriterFactory();
SSTableReader.Factory getReaderFactory();
RowIndexEntry.IndexSerializer<?> getIndexSerializer(CFMetaData cfm, Version version, SerializationHeader header);
public static enum Type
{
//Used internally to refer to files with no
//format flag in the filename
LEGACY("big", BigFormat.instance),
//The original sstable format
BIG("big", BigFormat.instance);
public final SSTableFormat info;
public final String name;
public static Type current()
{
return BIG;
}
@SuppressWarnings("deprecation")
private Type(String name, SSTableFormat info)
{
//Since format comes right after generation
//we disallow formats with numeric names
// We have removed this check for compatibility with the embedded cassandra used for tests.
assert !CharMatcher.digit().matchesAllOf(name);
this.name = name;
this.info = info;
}
public static Type validate(String name)
{
for (Type valid : Type.values())
{
//This is used internally for old sstables
if (valid == LEGACY)
continue;
if (valid.name.equalsIgnoreCase(name))
return valid;
}
throw new IllegalArgumentException("No Type constant " + name);
}
}
}

760
dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java

@ -1,760 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.apache.cassandra.io.util;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileStoreAttributeView;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.io.FSError;
import org.apache.cassandra.io.FSErrorHandler;
import org.apache.cassandra.io.FSReadError;
import org.apache.cassandra.io.FSWriteError;
import org.apache.cassandra.io.sstable.CorruptSSTableException;
import org.apache.cassandra.utils.JVMStabilityInspector;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static org.apache.cassandra.utils.Throwables.maybeFail;
import static org.apache.cassandra.utils.Throwables.merge;
public final class FileUtils
{
public static final Charset CHARSET = StandardCharsets.UTF_8;
private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);
public static final long ONE_KB = 1024;
public static final long ONE_MB = 1024 * ONE_KB;
public static final long ONE_GB = 1024 * ONE_MB;
public static final long ONE_TB = 1024 * ONE_GB;
private static final DecimalFormat df = new DecimalFormat("#.##");
public static final boolean isCleanerAvailable = false;
private static final AtomicReference<Optional<FSErrorHandler>> fsErrorHandler = new AtomicReference<>(Optional.empty());
public static void createHardLink(String from, String to)
{
createHardLink(new File(from), new File(to));
}
public static void createHardLink(File from, File to)
{
if (to.exists())
throw new RuntimeException("Tried to create duplicate hard link to " + to);
if (!from.exists())
throw new RuntimeException("Tried to hard link to file that does not exist " + from);
try
{
Files.createLink(to.toPath(), from.toPath());
}
catch (IOException e)
{
throw new FSWriteError(e, to);
}
}
public static File createTempFile(String prefix, String suffix, File directory)
{
try
{
return File.createTempFile(prefix, suffix, directory);
}
catch (IOException e)
{
throw new FSWriteError(e, directory);
}
}
public static File createTempFile(String prefix, String suffix)
{
return createTempFile(prefix, suffix, new File(System.getProperty("java.io.tmpdir")));
}
public static Throwable deleteWithConfirm(String filePath, boolean expect, Throwable accumulate)
{
return deleteWithConfirm(new File(filePath), expect, accumulate);
}
public static Throwable deleteWithConfirm(File file, boolean expect, Throwable accumulate)
{
boolean exists = file.exists();
assert exists || !expect : "attempted to delete non-existing file " + file.getName();
try
{
if (exists)
Files.delete(file.toPath());
}
catch (Throwable t)
{
try
{
throw new FSWriteError(t, file);
}
catch (Throwable t2)
{
accumulate = merge(accumulate, t2);
}
}
return accumulate;
}
public static void deleteWithConfirm(String file)
{
deleteWithConfirm(new File(file));
}
public static void deleteWithConfirm(File file)
{
maybeFail(deleteWithConfirm(file, true, null));
}
public static void renameWithOutConfirm(String from, String to)
{
try
{
atomicMoveWithFallback(new File(from).toPath(), new File(to).toPath());
}
catch (IOException e)
{
if (logger.isTraceEnabled())
logger.trace("Could not move file "+from+" to "+to, e);
}
}
public static void renameWithConfirm(String from, String to)
{
renameWithConfirm(new File(from), new File(to));
}
public static void renameWithConfirm(File from, File to)
{
assert from.exists();
if (logger.isTraceEnabled())
logger.trace("Renaming {} to {}", from.getPath(), to.getPath());
// this is not FSWE because usually when we see it it's because we didn't close the file before renaming it,
// and Windows is picky about that.
try
{
atomicMoveWithFallback(from.toPath(), to.toPath());
}
catch (IOException e)
{
throw new RuntimeException(String.format("Failed to rename %s to %s", from.getPath(), to.getPath()), e);
}
}
/**
* Move a file atomically, if it fails, it falls back to a non-atomic operation
* @param from
* @param to
* @throws IOException
*/
private static void atomicMoveWithFallback(Path from, Path to) throws IOException
{
try
{
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
}
catch (AtomicMoveNotSupportedException e)
{
logger.trace("Could not do an atomic move", e);
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
}
}
public static void truncate(String path, long size)
{
try(FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ, StandardOpenOption.WRITE))
{
channel.truncate(size);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public static void closeQuietly(Closeable c)
{
try
{
if (c != null)
c.close();
}
catch (Exception e)
{
logger.warn("Failed closing {}", c, e);
}
}
public static void closeQuietly(AutoCloseable c)
{
try
{
if (c != null)
c.close();
}
catch (Exception e)
{
logger.warn("Failed closing {}", c, e);
}
}
public static void close(Closeable... cs) throws IOException
{
close(Arrays.asList(cs));
}
public static void close(Iterable<? extends Closeable> cs) throws IOException
{
Throwable e = null;
for (Closeable c : cs)
{
try
{
if (c != null)
c.close();
}
catch (Throwable ex)
{
if (e == null) e = ex;
else e.addSuppressed(ex);
logger.warn("Failed closing stream {}", c, ex);
}
}
maybeFail(e, IOException.class);
}
public static void closeQuietly(Iterable<? extends AutoCloseable> cs)
{
for (AutoCloseable c : cs)
{
try
{
if (c != null)
c.close();
}
catch (Exception ex)
{
logger.warn("Failed closing {}", c, ex);
}
}
}
public static String getCanonicalPath(String filename)
{
try
{
return new File(filename).getCanonicalPath();
}
catch (IOException e)
{
throw new FSReadError(e, filename);
}
}
public static String getCanonicalPath(File file)
{
try
{
return file.getCanonicalPath();
}
catch (IOException e)
{
throw new FSReadError(e, file);
}
}
/** Return true if file is contained in folder */
public static boolean isContained(File folder, File file)
{
Path folderPath = Paths.get(getCanonicalPath(folder));
Path filePath = Paths.get(getCanonicalPath(file));
return filePath.startsWith(folderPath);
}
/** Convert absolute path into a path relative to the base path */
public static String getRelativePath(String basePath, String path)
{
try
{
return Paths.get(basePath).relativize(Paths.get(path)).toString();
}
catch(Exception ex)
{
String absDataPath = FileUtils.getCanonicalPath(basePath);
return Paths.get(absDataPath).relativize(Paths.get(path)).toString();
}
}
public static void clean(ByteBuffer buffer)
{
if (buffer == null)
return;
}
public static void createDirectory(String directory)
{
createDirectory(new File(directory));
}
public static void createDirectory(File directory)
{
if (!directory.exists())
{
if (!directory.mkdirs())
throw new FSWriteError(new IOException("Failed to mkdirs " + directory), directory);
}
}
public static boolean delete(String file)
{
File f = new File(file);
return f.delete();
}
public static void delete(File... files)
{
if (files == null)
{
// CASSANDRA-13389: some callers use Files.listFiles() which, on error, silently returns null
logger.debug("Received null list of files to delete");
return;
}
for ( File file : files )
{
file.delete();
}
}
public static void deleteAsync(final String file)
{
Runnable runnable = new Runnable()
{
public void run()
{
deleteWithConfirm(new File(file));
}
};
ScheduledExecutors.nonPeriodicTasks.execute(runnable);
}
public static void visitDirectory(Path dir, Predicate<? super File> filter, Consumer<? super File> consumer)
{
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir))
{
StreamSupport.stream(stream.spliterator(), false)
.map(Path::toFile)
// stream directories are weakly consistent so we always check if the file still exists
.filter(f -> f.exists() && (filter == null || filter.test(f)))
.forEach(consumer);
}
catch (IOException|DirectoryIteratorException ex)
{
logger.error("Failed to list files in {} with exception: {}", dir, ex.getMessage(), ex);
}
}
public static String stringifyFileSize(double value)
{
double d;
if ( value >= ONE_TB )
{
d = value / ONE_TB;
String val = df.format(d);
return val + " TiB";
}
else if ( value >= ONE_GB )
{
d = value / ONE_GB;
String val = df.format(d);
return val + " GiB";
}
else if ( value >= ONE_MB )
{
d = value / ONE_MB;
String val = df.format(d);
return val + " MiB";
}
else if ( value >= ONE_KB )
{
d = value / ONE_KB;
String val = df.format(d);
return val + " KiB";
}
else
{
String val = df.format(value);
return val + " bytes";
}
}
/**
* Deletes all files and subdirectories under "dir".
* @param dir Directory to be deleted
* @throws FSWriteError if any part of the tree cannot be deleted
*/
public static void deleteRecursive(File dir)
{
if (dir.isDirectory())
{
String[] children = dir.list();
for (String child : children)
deleteRecursive(new File(dir, child));
}
// The directory is now empty so now it can be smoked
deleteWithConfirm(dir);
}
/**
* Schedules deletion of all file and subdirectories under "dir" on JVM shutdown.
* @param dir Directory to be deleted
*/
public static void deleteRecursiveOnExit(File dir)
{
if (dir.isDirectory())
{
String[] children = dir.list();
for (String child : children)
deleteRecursiveOnExit(new File(dir, child));
}
logger.trace("Scheduling deferred deletion of file: {}", dir);
dir.deleteOnExit();
}
public static void handleCorruptSSTable(CorruptSSTableException e)
{
fsErrorHandler.get().ifPresent(handler -> handler.handleCorruptSSTable(e));
}
public static void handleFSError(FSError e)
{
fsErrorHandler.get().ifPresent(handler -> handler.handleFSError(e));
}
/**
* handleFSErrorAndPropagate will invoke the disk failure policy error handler,
* which may or may not stop the daemon or transports. However, if we don't exit,
* we still want to propagate the exception to the caller in case they have custom
* exception handling
*
* @param e A filesystem error
*/
public static void handleFSErrorAndPropagate(FSError e)
{
JVMStabilityInspector.inspectThrowable(e);
throwIfUnchecked(e);
throw new RuntimeException(e);
}
/**
* Get the size of a directory in bytes
* @param folder The directory for which we need size.
* @return The size of the directory
*/
public static long folderSize(File folder)
{
final long [] sizeArr = {0L};
try
{
Files.walkFileTree(folder.toPath(), new SimpleFileVisitor<Path>()
{
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
{
sizeArr[0] += attrs.size();
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException e)
{
logger.error("Error while getting {} folder size. {}", folder, e);
}
return sizeArr[0];
}
public static void copyTo(DataInput in, OutputStream out, int length) throws IOException
{
byte[] buffer = new byte[64 * 1024];
int copiedBytes = 0;
while (copiedBytes + buffer.length < length)
{
in.readFully(buffer);
out.write(buffer);
copiedBytes += buffer.length;
}
if (copiedBytes < length)
{
int left = length - copiedBytes;
in.readFully(buffer, 0, left);
out.write(buffer, 0, left);
}
}
public static boolean isSubDirectory(File parent, File child) throws IOException
{
parent = parent.getCanonicalFile();
child = child.getCanonicalFile();
File toCheck = child;
while (toCheck != null)
{
if (parent.equals(toCheck))
return true;
toCheck = toCheck.getParentFile();
}
return false;
}
public static void append(File file, String ... lines)
{
if (file.exists())
write(file, Arrays.asList(lines), StandardOpenOption.APPEND);
else
write(file, Arrays.asList(lines), StandardOpenOption.CREATE);
}
public static void appendAndSync(File file, String ... lines)
{
if (file.exists())
write(file, Arrays.asList(lines), StandardOpenOption.APPEND, StandardOpenOption.SYNC);
else
write(file, Arrays.asList(lines), StandardOpenOption.CREATE, StandardOpenOption.SYNC);
}
public static void replace(File file, String ... lines)
{
write(file, Arrays.asList(lines), StandardOpenOption.TRUNCATE_EXISTING);
}
public static void write(File file, List<String> lines, StandardOpenOption ... options)
{
try
{
Files.write(file.toPath(),
lines,
CHARSET,
options);
}
catch (IOException ex)
{
throw new RuntimeException(ex);
}
}
public static List<String> readLines(File file)
{
try
{
return Files.readAllLines(file.toPath(), CHARSET);
}
catch (IOException ex)
{
if (ex instanceof NoSuchFileException)
return Collections.emptyList();
throw new RuntimeException(ex);
}
}
public static void setFSErrorHandler(FSErrorHandler handler)
{
fsErrorHandler.getAndSet(Optional.ofNullable(handler));
}
/**
* Returns the size of the specified partition.
* <p>This method handles large file system by returning {@code Long.MAX_VALUE} if the size overflow.
* See <a href='https://bugs.openjdk.java.net/browse/JDK-8179320'>JDK-8179320</a> for more information.</p>
*
* @param file the partition
* @return the size, in bytes, of the partition or {@code 0L} if the abstract pathname does not name a partition
*/
public static long getTotalSpace(File file)
{
return handleLargeFileSystem(file.getTotalSpace());
}
/**
* Returns the number of unallocated bytes on the specified partition.
* <p>This method handles large file system by returning {@code Long.MAX_VALUE} if the number of unallocated bytes
* overflow. See <a href='https://bugs.openjdk.java.net/browse/JDK-8179320'>JDK-8179320</a> for more information</p>
*
* @param file the partition
* @return the number of unallocated bytes on the partition or {@code 0L}
* if the abstract pathname does not name a partition.
*/
public static long getFreeSpace(File file)
{
return handleLargeFileSystem(file.getFreeSpace());
}
/**
* Returns the number of available bytes on the specified partition.
* <p>This method handles large file system by returning {@code Long.MAX_VALUE} if the number of available bytes
* overflow. See <a href='https://bugs.openjdk.java.net/browse/JDK-8179320'>JDK-8179320</a> for more information</p>
*
* @param file the partition
* @return the number of available bytes on the partition or {@code 0L}
* if the abstract pathname does not name a partition.
*/
public static long getUsableSpace(File file)
{
return handleLargeFileSystem(file.getUsableSpace());
}
/**
* Returns the {@link FileStore} representing the file store where a file
* is located. This {@link FileStore} handles large file system by returning {@code Long.MAX_VALUE}
* from {@code FileStore#getTotalSpace()}, {@code FileStore#getUnallocatedSpace()} and {@code FileStore#getUsableSpace()}
* it the value is bigger than {@code Long.MAX_VALUE}. See <a href='https://bugs.openjdk.java.net/browse/JDK-8162520'>JDK-8162520</a>
* for more information.
*
* @param path the path to the file
* @return the file store where the file is stored
*/
public static FileStore getFileStore(Path path) throws IOException
{
return new SafeFileStore(Files.getFileStore(path));
}
/**
* Handle large file system by returning {@code Long.MAX_VALUE} when the size overflows.
* @param size returned by the Java's FileStore methods
* @return the size or {@code Long.MAX_VALUE} if the size was bigger than {@code Long.MAX_VALUE}
*/
private static long handleLargeFileSystem(long size)
{
return size < 0 ? Long.MAX_VALUE : size;
}
/**
* Private constructor as the class contains only static methods.
*/
private FileUtils()
{
}
/**
* FileStore decorator used to safely handle large file system.
*
* <p>Java's FileStore methods (getTotalSpace/getUnallocatedSpace/getUsableSpace) are limited to reporting bytes as
* signed long (2^63-1), if the filesystem is any bigger, then the size overflows. {@code SafeFileStore} will
* return {@code Long.MAX_VALUE} if the size overflow.</p>
*
* @see https://bugs.openjdk.java.net/browse/JDK-8162520.
*/
private static final class SafeFileStore extends FileStore
{
/**
* The decorated {@code FileStore}
*/
private final FileStore fileStore;
public SafeFileStore(FileStore fileStore)
{
this.fileStore = fileStore;
}
@Override
public String name()
{
return fileStore.name();
}
@Override
public String type()
{
return fileStore.type();
}
@Override
public boolean isReadOnly()
{
return fileStore.isReadOnly();
}
@Override
public long getTotalSpace() throws IOException
{
return handleLargeFileSystem(fileStore.getTotalSpace());
}
@Override
public long getUsableSpace() throws IOException
{
return handleLargeFileSystem(fileStore.getUsableSpace());
}
@Override
public long getUnallocatedSpace() throws IOException
{
return handleLargeFileSystem(fileStore.getUnallocatedSpace());
}
@Override
public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type)
{
return fileStore.supportsFileAttributeView(type);
}
@Override
public boolean supportsFileAttributeView(String name)
{
return fileStore.supportsFileAttributeView(name);
}
@Override
public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type)
{
return fileStore.getFileStoreAttributeView(type);
}
@Override
public Object getAttribute(String attribute) throws IOException
{
return fileStore.getAttribute(attribute);
}
}
}

96
dao/src/test/java/org/thingsboard/server/dao/AbstractNoSqlContainer.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
import com.github.dockerjava.api.command.InspectContainerResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.testcontainers.containers.CassandraContainer;
import org.testcontainers.containers.delegate.CassandraDatabaseDelegate;
import org.testcontainers.delegate.DatabaseDelegate;
import org.testcontainers.ext.ScriptUtils;
import javax.script.ScriptException;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
public abstract class AbstractNoSqlContainer {
public static final List<String> INIT_SCRIPTS = List.of(
"cassandra/schema-keyspace.cql",
"cassandra/schema-ts.cql",
"cassandra/schema-ts-latest.cql"
);
@ClassRule(order = 0)
public static final CassandraContainer cassandra = (CassandraContainer) new CassandraContainer("cassandra:4.1") {
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
super.containerIsStarted(containerInfo);
DatabaseDelegate db = new CassandraDatabaseDelegate(this);
INIT_SCRIPTS.forEach(script -> runInitScriptIfRequired(db, script));
}
private void runInitScriptIfRequired(DatabaseDelegate db, String initScriptPath) {
logger().info("Init script [{}]", initScriptPath);
if (initScriptPath != null) {
try {
URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath);
if (resource == null) {
logger().warn("Could not load classpath init script: {}", initScriptPath);
throw new ScriptUtils.ScriptLoadException("Could not load classpath init script: " + initScriptPath + ". Resource not found.");
}
String cql = IOUtils.toString(resource, StandardCharsets.UTF_8);
ScriptUtils.executeDatabaseScript(db, initScriptPath, cql);
} catch (IOException e) {
logger().warn("Could not load classpath init script: {}", initScriptPath);
throw new ScriptUtils.ScriptLoadException("Could not load classpath init script: " + initScriptPath, e);
} catch (ScriptException e) {
logger().error("Error while executing init script: {}", initScriptPath, e);
throw new ScriptUtils.UncategorizedScriptException("Error while executing init script: " + initScriptPath, e);
}
}
}
}
.withEnv("HEAP_NEWSIZE", "64M")
.withEnv("MAX_HEAP_SIZE", "512M")
.withEnv("CASSANDRA_CLUSTER_NAME", "ThingsBoard Cluster");
@ClassRule(order = 1)
public static ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
cassandra.start();
String cassandraUrl = String.format("%s:%s", cassandra.getHost(), cassandra.getMappedPort(9042));
log.debug("Cassandra url [{}]", cassandraUrl);
System.setProperty("cassandra.url", cassandraUrl);
}
@Override
protected void after() {
cassandra.stop();
List.of("cassandra.url")
.forEach(System.getProperties()::remove);
}
};
}

51
dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
import lombok.extern.slf4j.Slf4j;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.testcontainers.containers.GenericContainer;
import java.util.List;
@Slf4j
public class AbstractRedisContainer {
@ClassRule(order = 0)
public static GenericContainer redis = new GenericContainer("redis:7.0")
.withExposedPorts(6379);
@ClassRule(order = 1)
public static ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
redis.start();
System.setProperty("cache.type", "redis");
System.setProperty("redis.connection.type", "standalone");
System.setProperty("redis.standalone.host", redis.getHost());
System.setProperty("redis.standalone.port", String.valueOf(redis.getMappedPort(6379)));
}
@Override
protected void after() {
redis.stop();
List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port")
.forEach(System.getProperties()::remove);
}
};
}

88
dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java

@ -1,88 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
import com.datastax.oss.driver.api.core.CqlSession;
import org.cassandraunit.BaseCassandraUnit;
import org.cassandraunit.CQLDataLoader;
import org.cassandraunit.dataset.CQLDataSet;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import java.util.List;
public class CustomCassandraCQLUnit extends BaseCassandraUnit {
protected List<CQLDataSet> dataSets;
public CqlSession session;
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets) {
this.dataSets = dataSets;
}
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, int readTimeoutMillis) {
this.dataSets = dataSets;
this.readTimeoutMillis = readTimeoutMillis;
}
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName) {
this(dataSets);
this.configurationFileName = configurationFileName;
}
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, int readTimeoutMillis) {
this(dataSets);
this.configurationFileName = configurationFileName;
this.readTimeoutMillis = readTimeoutMillis;
}
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, long startUpTimeoutMillis) {
super(startUpTimeoutMillis);
this.dataSets = dataSets;
this.configurationFileName = configurationFileName;
}
public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, long startUpTimeoutMillis, int readTimeoutMillis) {
super(startUpTimeoutMillis);
this.dataSets = dataSets;
this.configurationFileName = configurationFileName;
this.readTimeoutMillis = readTimeoutMillis;
}
@Override
protected void load() {
session = EmbeddedCassandraServerHelper.getSession();
CQLDataLoader dataLoader = new CQLDataLoader(session);
dataSets.forEach(dataLoader::load);
session = dataLoader.getSession();
System.setSecurityManager(null);
}
@Override
protected void after() {
super.after();
try (CqlSession s = session) {
session = null;
}
System.setSecurityManager(null);
}
// Getters for those who do not like to directly access fields
public CqlSession getSession() {
return session;
}
}

16
dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java

@ -15,28 +15,14 @@
*/
package org.thingsboard.server.dao;
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.service.*.nosql.*ServiceNoSqlTest",
})
public class NoSqlDaoServiceTestSuite {
@ClassRule
public static CustomCassandraCQLUnit cassandraUnit =
new CustomCassandraCQLUnit(
Arrays.asList(
new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false),
new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false)
),
"cassandra-test.yaml", 30000L);
public class NoSqlDaoServiceTestSuite extends AbstractNoSqlContainer {
}

24
dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java

@ -15,37 +15,15 @@
*/
package org.thingsboard.server.dao;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.testcontainers.containers.GenericContainer;
@ContextConfiguration(initializers = RedisSqlTestSuite.class)
@RunWith(ClasspathSuite.class)
@ClassnameFilters(
//All the same tests using redis instead of caffeine.
"org.thingsboard.server.dao.service.*ServiceSqlTest"
)
public class RedisSqlTestSuite implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379);
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext, "cache.type=redis");
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext, "redis.connection.type=standalone");
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext, "redis.standalone.host=localhost");
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379));
}
public class RedisSqlTestSuite extends AbstractRedisContainer {
}

4
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java

@ -43,8 +43,8 @@ import static org.mockito.Mockito.when;
public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest {
private static final String CREDENTIALS_ID_1 = StringUtils.randomAlphanumeric(20);
private static final String CREDENTIALS_ID_2 = StringUtils.randomAlphanumeric(20);
private final String CREDENTIALS_ID_1 = StringUtils.randomAlphanumeric(20);
private final String CREDENTIALS_ID_2 = StringUtils.randomAlphanumeric(20);
@Autowired
private DeviceCredentialsService deviceCredentialsService;

360
dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java

@ -26,7 +26,6 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@ -50,6 +49,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AssetSearchQueryFilter;
import org.thingsboard.server.common.data.query.AssetTypeFilter;
import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter;
@ -62,6 +62,7 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityNameFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@ -106,9 +107,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
private TenantId tenantId;
@Autowired
private JdbcTemplate template;
@Autowired
private RelationRepository relationRepository;
@ -986,6 +984,360 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
assertEquals(devices.size(), result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_entity_name_starts_with() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device " + i + " test");
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
EntityNameFilter deviceTypeFilter = new EntityNameFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
deviceTypeFilter.setEntityNameFilter("Device");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("Device%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("%Device%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("%Device");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_entity_name_ends_with() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device " + i + " test");
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
EntityNameFilter deviceTypeFilter = new EntityNameFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
deviceTypeFilter.setEntityNameFilter("%test");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("%test%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_entity_name_contains() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device test" + i);
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
EntityNameFilter deviceTypeFilter = new EntityNameFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
deviceTypeFilter.setEntityNameFilter("%test%");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
deviceTypeFilter.setEntityNameFilter("%test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_device_type_name_starts_with() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device " + i + " test");
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceType("default");
deviceTypeFilter.setDeviceNameFilter("Device");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("Device%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("%Device%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("%Device");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_device_type_name_ends_with() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device " + i + " test");
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceType("default");
deviceTypeFilter.setDeviceNameFilter("%test");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("%test%");
result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_device_type_name_contains() {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device test" + i);
device.setType("default");
devices.add(device);
}
devices.forEach(deviceService::saveDevice);
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceType("default");
deviceTypeFilter.setDeviceNameFilter("%test%");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(devices.size(), result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
deviceTypeFilter.setDeviceNameFilter("%test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_asset_type_name_starts_with() {
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset " + i + " test");
asset.setType("default");
assets.add(asset);
}
assets.forEach(assetService::saveAsset);
AssetTypeFilter assetTypeFilter = new AssetTypeFilter();
assetTypeFilter.setAssetType("default");
assetTypeFilter.setAssetNameFilter("Asset");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("Asset%");
result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("%Asset%");
result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("%Asset");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_asset_type_name_ends_with() {
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset " + i + " test");
asset.setType("default");
assets.add(asset);
}
assets.forEach(assetService::saveAsset);
AssetTypeFilter assetTypeFilter = new AssetTypeFilter();
assetTypeFilter.setAssetType("default");
assetTypeFilter.setAssetNameFilter("%test");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("%test%");
result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
assetTypeFilter.setAssetNameFilter("test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
@Test
public void testFindEntityDataByQuery_filter_asset_type_name_contains() {
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset test" + i);
asset.setType("default");
assets.add(asset);
}
assets.forEach(assetService::saveAsset);
AssetTypeFilter assetTypeFilter = new AssetTypeFilter();
assetTypeFilter.setAssetType("default");
assetTypeFilter.setAssetNameFilter("%test%");
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
PageData<EntityData> result = searchEntities(query);
assertEquals(assets.size(), result.getTotalElements());
assetTypeFilter.setAssetNameFilter("test%");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
assetTypeFilter.setAssetNameFilter("%test");
result = searchEntities(query);
assertEquals(0, result.getTotalElements());
}
private PageData<EntityData> searchEntities(EntityDataQuery query) {
return entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
}

3
dao/src/test/resources/application-test.properties

@ -7,10 +7,9 @@ updates.enabled=false
audit-log.enabled=true
audit-log.sink.type=none
cache.type=caffeine
#cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine)
cache.maximumPoolSize=16
cache.attributes.enabled=true
#cache.type=redis
cache.specs.relations.timeToLiveInMinutes=1440
cache.specs.relations.maxSize=100000

2
dao/src/test/resources/cassandra-test.properties

@ -2,7 +2,7 @@ cassandra.cluster_name=Thingsboard Cluster
cassandra.keyspace_name=thingsboard
cassandra.url=127.0.0.1:9142
#cassandra.url=127.0.0.1:9142 # will be injected by NoSqlContainer
cassandra.local_datacenter=datacenter1

4
dao/src/test/resources/logback.xml

@ -8,9 +8,7 @@
</appender>
<logger name="org.thingsboard.server.dao" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="WARN" />
<logger name="org.apache.cassandra" level="WARN" />
<logger name="org.testcontainers" level="INFO" />
<root level="WARN">
<appender-ref ref="console"/>

2
dao/src/test/resources/nosql-test.properties

@ -15,7 +15,7 @@ spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.hikari.maximumPoolSize = 50
spring.datasource.hikari.maximumPoolSize=16
queue.rule-engine.queues[0].name=Main
queue.rule-engine.queues[0].topic=tb_rule_engine.main

2
dao/src/test/resources/sql-test.properties

@ -16,7 +16,7 @@ spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.hikari.maximumPoolSize = 50
spring.datasource.hikari.maximumPoolSize=16
service.type=monolith

18
msa/black-box-tests/README.md

@ -18,7 +18,7 @@ As result, in REPOSITORY column, next images should be present:
thingsboard/tb-web-ui
thingsboard/tb-js-executor
- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone:
- Run the black box tests (without ui tests) in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone:
mvn clean install -DblackBoxTests.skip=false
@ -34,11 +34,23 @@ As result, in REPOSITORY column, next images should be present:
mvn clean install -DblackBoxTests.skip=false -DrunLocal=true
- To run ui smoke tests in the [msa/black-box-tests](../black-box-tests) directory specifying suite name:
- To run only ui tests in the [msa/black-box-tests](../black-box-tests) directory:
mvn clean install -DblackBoxTests.skip=false -Dsuite=uiTests
- To run all tests in the [msa/black-box-tests](../black-box-tests) directory specifying suite name:
- To run only ui smoke rule chains tests in the [msa/black-box-tests](../black-box-tests) directory:
mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesRuleChain
- To run only ui smoke customers tests in the [msa/black-box-tests](../black-box-tests) directory:
mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesCustomer
- To run only ui smoke profiles tests in the [msa/black-box-tests](../black-box-tests) directory:
mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesPrifiles
- To run all tests (black-box and ui) in the [msa/black-box-tests](../black-box-tests) directory:
mvn clean install -DblackBoxTests.skip=false -Dsuite=all

1
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -106,6 +106,7 @@ public class ContainerTestSuite {
new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid-test-extras.yml" : "docker-compose.postgres-test-extras.yml")),
new File(targetDir + "docker-compose.postgres.volumes.yml"),
new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")),

5
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java

@ -16,6 +16,7 @@
package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.DockerClientFactory;
import java.io.IOException;
import java.io.InputStream;
@ -41,7 +42,9 @@ public class TestProperties {
public static String getBaseUiUrl() {
if (instance.isActive()) {
return "https://host.docker.internal";
//return "https://host.docker.internal" // this alternative requires docker-selenium.yml extra_hosts: - "host.docker.internal:host-gateway"
//return "https://" + DockerClientFactory.instance().dockerHostIpAddress(); //this alternative will get Docker IP from testcontainers
return "https://haproxy"; //communicate inside current docker-compose network to the load balancer container
}
return getProperties().getProperty("tb.baseUiUrl");
}

3
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java

@ -82,6 +82,9 @@ public class ThingsBoardDbInstaller {
));
if (IS_HYBRID_MODE) {
composeFiles.add(new File("./../../docker/docker-compose.cassandra.volumes.yml"));
composeFiles.add(new File("src/test/resources/docker-compose.hybrid-test-extras.yml"));
} else {
composeFiles.add(new File("src/test/resources/docker-compose.postgres-test-extras.yml"));
}
String identifier = Base58.randomString(6).toLowerCase();

4
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java

@ -32,9 +32,11 @@ import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@Slf4j
abstract public class AbstractBasePage {
public static final long WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
protected WebDriver driver;
protected WebDriverWait wait;
protected Actions actions;
@ -43,7 +45,7 @@ abstract public class AbstractBasePage {
public AbstractBasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofMillis(8000));
this.wait = new WebDriverWait(driver, Duration.ofMillis(WAIT_TIMEOUT));
this.actions = new Actions(driver);
this.js = (JavascriptExecutor) driver;
}

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java

@ -42,7 +42,7 @@ public class OtherPageElements extends AbstractBasePage {
private static final String MARKS_CHECKBOX = "//mat-row[contains (@class,'mat-selected')]//mat-checkbox[contains(@class, 'checked')]";
private static final String SELECT_ALL_CHECKBOX = "//thead//mat-checkbox";
private static final String ALL_ENTITY = "//mat-row[@class='mat-row cdk-row mat-row-select ng-star-inserted']";
private static final String EDIT_PENCIL_BTN = "//mat-icon[contains(text(),'edit')]/ancestor::button";
private static final String EDIT_PENCIL_BTN = "//tb-details-panel//mat-icon[contains(text(),'edit')]/ancestor::button";
private static final String NAME_FIELD_EDIT_VIEW = "//input[@formcontrolname='name']";
private static final String HEADER_NAME_VIEW = "//header//div[@class='tb-details-title']/span";
private static final String DONE_BTN_EDIT_VIEW = "//mat-icon[contains(text(),'done')]/ancestor::button";

23
msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml

@ -0,0 +1,23 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
version: '3.0'
services:
cassandra:
environment:
HEAP_NEWSIZE: 128M
MAX_HEAP_SIZE: 1024M

19
msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml

@ -0,0 +1,19 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
version: '3.0'
# Placeholder

2
msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml

@ -14,7 +14,7 @@
# limitations under the License.
#
version: '2.2'
version: '3.0'
services:
rabbitmq:

7
msa/black-box-tests/src/test/resources/docker-selenium.yml

@ -31,7 +31,6 @@ services:
SE_SCREEN_HEIGHT: 1080
SE_SCREEN_DEPTH: 24
SE_SCREEN_DPI: 74
extra_hosts:
- "host.docker.internal:172.17.0.1"
# Alternative way how to connect to the host address
# extra_hosts:
# - "host.docker.internal:host-gateway"

5
msa/pom.xml

@ -39,13 +39,14 @@
</properties>
<modules>
<!--Modules order is important to speedup parallel build and avoid yarn pgk parallel execution-->
<module>tb</module>
<module>web-ui</module>
<module>vc-executor</module>
<module>vc-executor-docker</module>
<module>js-executor</module>
<module>web-ui</module>
<module>tb-node</module>
<module>transport</module>
<module>js-executor</module>
</modules>
<profiles>

31
pom.xml

@ -85,7 +85,7 @@
<netty-tcnative.version>2.0.51.Final</netty-tcnative.version>
<os-maven-plugin.version>1.7.0</os-maven-plugin.version>
<rabbitmq.version>4.8.0</rabbitmq.version>
<surefire.version>3.0.0-M6</surefire.version>
<surefire.version>3.0.0-M9</surefire.version>
<jar-plugin.version>3.0.2</jar-plugin.version>
<springfox-swagger.version>3.0.4</springfox-swagger.version>
<swagger-annotations.version>1.6.3</swagger-annotations.version>
@ -126,7 +126,6 @@
<snmp4j.version>2.8.5</snmp4j.version>
<!-- TEST SCOPE -->
<awaitility.version>4.1.0</awaitility.version>
<cassandra-unit.version>4.3.1.0</cassandra-unit.version>
<dbunit.version>2.7.2</dbunit.version>
<java-websocket.version>1.5.2</java-websocket.version>
<jupiter.version>5.8.2</jupiter.version> <!-- keep the same version as spring-boot-starter-test depend on jupiter-->
@ -660,7 +659,7 @@
<version>${surefire.version}</version>
<configuration>
<argLine>
--illegal-access=permit
--illegal-access=permit -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20
</argLine>
</configuration>
</plugin>
@ -1604,26 +1603,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<version>${cassandra-unit.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
@ -1747,6 +1726,12 @@
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cassandra</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>

16
rule-engine/rule-engine-components/pom.xml

@ -150,22 +150,6 @@
<artifactId>mockserver-client-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>

41
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java

@ -57,6 +57,9 @@ import java.security.cert.X509Certificate;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -104,7 +107,7 @@ public class CertPemCredentials implements ClientCredentials {
}
private KeyManagerFactory createAndInitKeyManagerFactory() throws Exception {
X509Certificate certHolder = readCertFile(cert);
List<X509Certificate> certHolders = readCertFile(cert);
Object keyObject = readPrivateKeyFile(privateKey);
char[] passwordCharArray = "".toCharArray();
if (!StringUtils.isEmpty(password)) {
@ -129,43 +132,53 @@ public class CertPemCredentials implements ClientCredentials {
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
clientKeyStore.load(null, null);
clientKeyStore.setCertificateEntry("cert", certHolder);
for (X509Certificate certHolder : certHolders) {
clientKeyStore.setCertificateEntry("cert-" + certHolder.getSubjectDN().getName(), certHolder);
}
clientKeyStore.setKeyEntry("private-key",
privateKey,
passwordCharArray,
new Certificate[]{certHolder});
certHolders.toArray(new Certificate[]{}));
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, passwordCharArray);
return keyManagerFactory;
}
protected TrustManagerFactory createAndInitTrustManagerFactory() throws Exception {
X509Certificate caCertHolder;
caCertHolder = readCertFile(caCert);
List<X509Certificate> caCertHolders = readCertFile(caCert);
KeyStore caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
caKeyStore.load(null, null);
caKeyStore.setCertificateEntry("caCert-cert", caCertHolder);
for (X509Certificate caCertHolder : caCertHolders) {
caKeyStore.setCertificateEntry("caCert-cert-" + caCertHolder.getSubjectDN().getName(), caCertHolder);
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(caKeyStore);
return trustManagerFactory;
}
private X509Certificate readCertFile(String fileContent) throws Exception {
X509Certificate certificate = null;
if (fileContent != null && !fileContent.trim().isEmpty()) {
fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
List<X509Certificate> readCertFile(String fileContent) throws Exception {
if (fileContent == null || fileContent.trim().isEmpty()) {
return Collections.emptyList();
}
List<X509Certificate> certificates = new ArrayList<>();
String[] pems = fileContent.trim().split("-----END CERTIFICATE-----");
for (String pem : pems) {
if (pem.trim().isEmpty()) {
continue;
}
pem = pem.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.decodeBase64(fileContent);
byte[] decoded = Base64.decodeBase64(pem);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
try (InputStream inStream = new ByteArrayInputStream(decoded)) {
certificate = (X509Certificate) certFactory.generateCertificate(inStream);
certificates.add((X509Certificate) certFactory.generateCertificate(inStream));
}
}
return certificate;
return certificates;
}
private PrivateKey readPrivateKeyFile(String fileContent) throws Exception {

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

73
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/credentials/CertPemCredentialsTest.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.credentials;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.List;
public class CertPemCredentialsTest {
private final CertPemCredentials credentials = new CertPemCredentials();
@Test
public void testChainOfCertificates() throws Exception {
String fileContent = fileContent("pem/tb-cloud-chain.pem");
List<X509Certificate> x509Certificates = credentials.readCertFile(fileContent);
Assert.assertEquals(4, x509Certificates.size());
Assert.assertEquals("CN=*.thingsboard.cloud, O=\"ThingsBoard, Inc.\", ST=New York, C=US",
x509Certificates.get(0).getSubjectDN().getName());
Assert.assertEquals("CN=Sectigo ECC Organization Validation Secure Server CA, O=Sectigo Limited, L=Salford, ST=Greater Manchester, C=GB",
x509Certificates.get(1).getSubjectDN().getName());
Assert.assertEquals("CN=USERTrust ECC Certification Authority, O=The USERTRUST Network, L=Jersey City, ST=New Jersey, C=US",
x509Certificates.get(2).getSubjectDN().getName());
Assert.assertEquals("CN=AAA Certificate Services, O=Comodo CA Limited, L=Salford, ST=Greater Manchester, C=GB",
x509Certificates.get(3).getSubjectDN().getName());
}
@Test
public void testSingleCertificate() throws Exception {
String fileContent = fileContent("pem/tb-cloud.pem");
List<X509Certificate> x509Certificates = credentials.readCertFile(fileContent);
Assert.assertEquals(1, x509Certificates.size());
Assert.assertEquals("CN=*.thingsboard.cloud, O=\"ThingsBoard, Inc.\", ST=New York, C=US",
x509Certificates.get(0).getSubjectDN().getName());
}
@Test
public void testEmptyFileContent() throws Exception {
String fileContent = fileContent("pem/empty.pem");
List<X509Certificate> x509Certificates = credentials.readCertFile(fileContent);
Assert.assertEquals(0, x509Certificates.size());
}
private String fileContent(String fileName) throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
return FileUtils.readFileToString(file, "UTF-8");
}
}

0
rule-engine/rule-engine-components/src/test/resources/pem/empty.pem

103
rule-engine/rule-engine-components/src/test/resources/pem/tb-cloud-chain.pem

@ -0,0 +1,103 @@
-----BEGIN CERTIFICATE-----
MIIFejCCBSCgAwIBAgIQT2YV5NVp2PAY1O5rxMlj6DAKBggqhkjOPQQDAjCBlTEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMT0wOwYDVQQDEzRT
ZWN0aWdvIEVDQyBPcmdhbml6YXRpb24gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVy
IENBMB4XDTIxMTAwNTAwMDAwMFoXDTIyMTAwNTIzNTk1OVowWjELMAkGA1UEBhMC
VVMxETAPBgNVBAgTCE5ldyBZb3JrMRowGAYDVQQKExFUaGluZ3NCb2FyZCwgSW5j
LjEcMBoGA1UEAwwTKi50aGluZ3Nib2FyZC5jbG91ZDB2MBAGByqGSM49AgEGBSuB
BAAiA2IABNkK/UerQZXPP0H/Tl8YhRPlzW85yTAcQ5hXhs2fyXn7Bdj4EueQuZrv
Pw98xwHJr87jslFbS/WiSdtBYPvjsUyXqh7aMvOcEhSgEOWDmtoj3P1Xk1hNLb6m
xAQfFL8cZ6OCA20wggNpMB8GA1UdIwQYMBaAFE1K78RGsxKtT06asVniUasIEHgI
MB0GA1UdDgQWBBRr4HG23dsao68r9r7obGxB70ptBzAOBgNVHQ8BAf8EBAMCB4Aw
DAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwSgYD
VR0gBEMwQTA1BgwrBgEEAbIxAQIBAwQwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9z
ZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQICMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb0VDQ09yZ2FuaXphdGlvblZhbGlkYXRp
b25TZWN1cmVTZXJ2ZXJDQS5jcmwwgYoGCCsGAQUFBwEBBH4wfDBVBggrBgEFBQcw
AoZJaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvRUNDT3JnYW5pemF0aW9u
VmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYXaHR0cDov
L29jc3Auc2VjdGlnby5jb20wMQYDVR0RBCowKIITKi50aGluZ3Nib2FyZC5jbG91
ZIIRdGhpbmdzYm9hcmQuY2xvdWQwggGABgorBgEEAdZ5AgQCBIIBcASCAWwBagB2
AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr8vxw/m1HAAABfFDbhtMAAAQDAEcw
RQIhAKNykhkQTngK0yOcYHGHUQSy6JmJYl+5nc1qELirPHwAAiBgV2Db5ZFHNvzn
zp9Ob/OG0o36z6rcilbLI/daZwnyewB3AEHIyrHfIkZKEMahOglCh15OMYsbA+vr
S8do8JBilgb2AAABfFDbhqYAAAQDAEgwRgIhALFvTbapKhO7DPrF6KtE9sjFMMth
qjqaeaHYN6JGnUAIAiEApEW+rxlzxH1+qEwJrFyQLr5rSKTuEoSjv3hbrzb9GQ4A
dwApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXxQ24ZnAAAEAwBI
MEYCIQDReSpJPzADl/fBdCvyZwWu3Ubi6y0h/S4i7RIjf5L5pAIhAJ1oUmNmRWQL
1U3HAqz2V8ckH/rgA0BR+CkGBigt3dfwMAoGCCqGSM49BAMCA0gAMEUCID0Wv3hJ
GyO4kxlCsA/Kruzew8Wr/0k84csyaCo0k16kAiEAtAobCIzx/PIDWU2rX5elBNiR
13sr0/ED+2PUom2dnfg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDrjCCAzOgAwIBAgIQNb50Y4yz6d4oBXC3l4CzZzAKBggqhkjOPQQDAzCBiDEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl
eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT
JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgxMTAy
MDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBlTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMP
U2VjdGlnbyBMaW1pdGVkMT0wOwYDVQQDEzRTZWN0aWdvIEVDQyBPcmdhbml6YXRp
b24gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMFkwEwYHKoZIzj0CAQYIKoZI
zj0DAQcDQgAEnI5cCmFvoVij0NXO+vxE+f+6Bh57FhpyH0LTCrJmzfsPSXIhTSex
r92HOlz+aHqoGE0vSe/CSwLFoWcZ8W1jOaOCAW4wggFqMB8GA1UdIwQYMBaAFDrh
CYbUzxnClnZ0SXbc4DXGY2OaMB0GA1UdDgQWBBRNSu/ERrMSrU9OmrFZ4lGrCBB4
CDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAU
BggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEC
AjBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNF
UlRydXN0RUNDQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEE
ajBoMD8GCCsGAQUFBzAChjNodHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRy
dXN0RUNDQWRkVHJ1c3RDQS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVz
ZXJ0cnVzdC5jb20wCgYIKoZIzj0EAwMDaQAwZgIxAOk//uo7i/MoeKdcyeqvjOXs
BJFGLI+1i0d+Tty7zEnn2w4DNS21TK8wmY3Kjm3EmQIxAPI1qHM/I+OS+hx0OZhG
fDoNifTe/GxgWZ1gOYQKzn6lwP0yGKlrP+7vrVC8IczJ4A==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0
aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL
q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc
JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA
FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1
xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI
MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j
b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM
BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy
ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+
FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV
bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY
CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf
8qn0dNW44bOwgeThpWOjzOoEeJBuv/c=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
-----END CERTIFICATE-----

32
rule-engine/rule-engine-components/src/test/resources/pem/tb-cloud.pem

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFejCCBSCgAwIBAgIQT2YV5NVp2PAY1O5rxMlj6DAKBggqhkjOPQQDAjCBlTEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMT0wOwYDVQQDEzRT
ZWN0aWdvIEVDQyBPcmdhbml6YXRpb24gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVy
IENBMB4XDTIxMTAwNTAwMDAwMFoXDTIyMTAwNTIzNTk1OVowWjELMAkGA1UEBhMC
VVMxETAPBgNVBAgTCE5ldyBZb3JrMRowGAYDVQQKExFUaGluZ3NCb2FyZCwgSW5j
LjEcMBoGA1UEAwwTKi50aGluZ3Nib2FyZC5jbG91ZDB2MBAGByqGSM49AgEGBSuB
BAAiA2IABNkK/UerQZXPP0H/Tl8YhRPlzW85yTAcQ5hXhs2fyXn7Bdj4EueQuZrv
Pw98xwHJr87jslFbS/WiSdtBYPvjsUyXqh7aMvOcEhSgEOWDmtoj3P1Xk1hNLb6m
xAQfFL8cZ6OCA20wggNpMB8GA1UdIwQYMBaAFE1K78RGsxKtT06asVniUasIEHgI
MB0GA1UdDgQWBBRr4HG23dsao68r9r7obGxB70ptBzAOBgNVHQ8BAf8EBAMCB4Aw
DAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwSgYD
VR0gBEMwQTA1BgwrBgEEAbIxAQIBAwQwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9z
ZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQICMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb0VDQ09yZ2FuaXphdGlvblZhbGlkYXRp
b25TZWN1cmVTZXJ2ZXJDQS5jcmwwgYoGCCsGAQUFBwEBBH4wfDBVBggrBgEFBQcw
AoZJaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvRUNDT3JnYW5pemF0aW9u
VmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYXaHR0cDov
L29jc3Auc2VjdGlnby5jb20wMQYDVR0RBCowKIITKi50aGluZ3Nib2FyZC5jbG91
ZIIRdGhpbmdzYm9hcmQuY2xvdWQwggGABgorBgEEAdZ5AgQCBIIBcASCAWwBagB2
AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr8vxw/m1HAAABfFDbhtMAAAQDAEcw
RQIhAKNykhkQTngK0yOcYHGHUQSy6JmJYl+5nc1qELirPHwAAiBgV2Db5ZFHNvzn
zp9Ob/OG0o36z6rcilbLI/daZwnyewB3AEHIyrHfIkZKEMahOglCh15OMYsbA+vr
S8do8JBilgb2AAABfFDbhqYAAAQDAEgwRgIhALFvTbapKhO7DPrF6KtE9sjFMMth
qjqaeaHYN6JGnUAIAiEApEW+rxlzxH1+qEwJrFyQLr5rSKTuEoSjv3hbrzb9GQ4A
dwApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXxQ24ZnAAAEAwBI
MEYCIQDReSpJPzADl/fBdCvyZwWu3Ubi6y0h/S4i7RIjf5L5pAIhAJ1oUmNmRWQL
1U3HAqz2V8ckH/rgA0BR+CkGBigt3dfwMAoGCCqGSM49BAMCA0gAMEUCID0Wv3hJ
GyO4kxlCsA/Kruzew8Wr/0k84csyaCo0k16kAiEAtAobCIzx/PIDWU2rX5elBNiR
13sr0/ED+2PUom2dnfg=
-----END CERTIFICATE-----

16
ui-ngx/.browserslistrc

@ -1,16 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

3
ui-ngx/angular.json

@ -145,7 +145,8 @@
"ace",
"ace-builds",
"diff-match-patch",
"tv4"
"tv4",
"@messageformat/parser"
]
},
"configurations": {

71
ui-ngx/package.json

@ -14,17 +14,17 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^14.2.12",
"@angular/cdk": "^14.2.7",
"@angular/common": "^14.2.12",
"@angular/compiler": "^14.2.12",
"@angular/core": "^14.2.12",
"@angular/flex-layout": "^14.0.0-beta.41",
"@angular/forms": "^14.2.12",
"@angular/material": "^14.2.7",
"@angular/platform-browser": "^14.2.12",
"@angular/platform-browser-dynamic": "^14.2.12",
"@angular/router": "^14.2.12",
"@angular/animations": "^15.2.0",
"@angular/cdk": "^15.2.0",
"@angular/common": "^15.2.0",
"@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.0",
"@angular/material": "^15.2.0",
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"@auth0/angular-jwt": "^5.1.2",
"@date-io/core": "1.3.7",
"@date-io/date-fns": "1.3.7",
@ -32,19 +32,20 @@
"@flowjs/ngx-flow": "~0.6.0",
"@geoman-io/leaflet-geoman-free": "^2.13.0",
"@juggle/resize-observer": "^3.3.1",
"@mat-datetimepicker/core": "~10.1.1",
"@mat-datetimepicker/core": "~11.0.3",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/pickers": "3.3.10",
"@ngrx/effects": "^14.3.3",
"@ngrx/store": "^14.3.3",
"@ngrx/store-devtools": "^14.3.3",
"@messageformat/core": "^3.0.1",
"@ngrx/effects": "^15.3.0",
"@ngrx/store": "^15.3.0",
"@ngrx/store-devtools": "^15.3.0",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",
"angular-gridster2": "~14.1.4",
"angular-gridster2": "~15.0.3",
"angular2-hotkeys": "^13.1.0",
"canvas-gauges": "^2.1.7",
"core-js": "^3.26.1",
@ -67,18 +68,18 @@
"leaflet.gridlayer.googlemutant": "^0.13.5",
"leaflet.markercluster": "^1.5.3",
"libphonenumber-js": "^1.10.4",
"messageformat": "^2.3.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.40",
"marked": "^4.0.17",
"moment": "^2.29.4",
"moment-timezone": "^0.5.41",
"ngx-clipboard": "^15.1.0",
"ngx-color-picker": "^13.0.0",
"ngx-daterangepicker-material": "^6.0.4",
"ngx-drag-drop": "^14.0.0",
"ngx-drag-drop": "^15.0.1",
"ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/2.0.0",
"ngx-hm-carousel": "^3.0.0",
"ngx-markdown": "^14.0.1",
"ngx-markdown": "^15.1.1",
"ngx-sharebuttons": "^11.0.0",
"ngx-translate-messageformat-compiler": "^5.1.0",
"ngx-translate-messageformat-compiler": "^6.2.0",
"objectpath": "^2.0.0",
"prettier": "^2.8.3",
"prop-types": "^15.8.1",
@ -105,17 +106,17 @@
"zone.js": "~0.11.8"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~14.1.0",
"@angular-devkit/build-angular": "^14.2.10",
"@angular-eslint/builder": "14.4.0",
"@angular-eslint/eslint-plugin": "14.4.0",
"@angular-eslint/eslint-plugin-template": "14.4.0",
"@angular-eslint/schematics": "14.4.0",
"@angular-eslint/template-parser": "14.4.0",
"@angular/cli": "^14.2.10",
"@angular/compiler-cli": "^14.2.12",
"@angular/language-service": "^14.2.12",
"@ngtools/webpack": "^14.2.10",
"@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "^15.2.0",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.0",
"@angular/compiler-cli": "^15.2.0",
"@angular/language-service": "^15.2.0",
"@ngtools/webpack": "^15.1.6",
"@types/ace-diff": "^2.1.1",
"@types/canvas-gauges": "^2.1.4",
"@types/flot": "^0.0.32",
@ -131,7 +132,7 @@
"@types/leaflet.gridlayer.googlemutant": "^0.4.6",
"@types/leaflet.markercluster": "^1.5.1",
"@types/lodash": "^4.14.177",
"@types/moment-timezone": "^0.5.30",
"@types/marked": "^4.0.3",
"@types/mousetrap": "^1.6.0",
"@types/node": "~15.14.9",
"@types/raphael": "^2.3.2",
@ -161,7 +162,7 @@
"protractor": "~7.0.0",
"raw-loader": "^4.0.2",
"ts-node": "^10.9.1",
"typescript": "~4.6.4",
"typescript": "~4.8.4",
"webpack": "^5.75.0"
},
"resolutions": {

11
ui-ngx/src/app/core/translate/translate-default-compiler.ts

@ -20,7 +20,7 @@ import {
TranslateMessageFormatCompiler
} from 'ngx-translate-messageformat-compiler';
import { Inject, Injectable, Optional } from '@angular/core';
import messageFormatParser from 'messageformat-parser';
import { parse } from '@messageformat/parser';
@Injectable({ providedIn: 'root' })
export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
@ -44,7 +44,12 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
private defaultCompile(src: any, lang: string): any {
if (typeof src !== 'object') {
if (this.checkIsPlural(src)) {
return super.compile(src, lang);
try {
return super.compile(src, lang.replace('_', '-'));
} catch (e) {
console.warn('Failed compile translate:', src, e);
return src;
}
} else {
return src;
}
@ -60,7 +65,7 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
private checkIsPlural(src: string): boolean {
let tokens: any[];
try {
tokens = messageFormatParser.parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
tokens = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
{cardinal: [], ordinal: []});
} catch (e) {
console.warn(`Failed to parse source: ${src}`);

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

@ -304,7 +304,7 @@ export function deepClone<T>(target: T, ignoreFields?: string[]): T {
(target as any[]).forEach((v) => { cp.push(v); });
return cp.map((n: any) => deepClone<any>(n)) as any;
}
if (typeof target === 'object' && target !== {}) {
if (typeof target === 'object' && Object.keys(target).length) {
const cp = {...(target as { [key: string]: any })} as { [key: string]: any };
Object.keys(cp).forEach(k => {
if (!ignoreFields || ignoreFields.indexOf(k) === -1) {

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -98,7 +98,6 @@ import * as LogoComponent from '@shared/components/logo.component';
import * as FooterFabButtonsComponent from '@shared/components/footer-fab-buttons.component';
import * as FullscreenDirective from '@shared/components/fullscreen.directive';
import * as CircularProgressDirective from '@shared/components/circular-progress.directive';
import * as MatChipDraggableDirective from '@shared/components/mat-chip-draggable.directive';
import * as TbHotkeysDirective from '@shared/components/hotkeys.directive';
import * as TbAnchorComponent from '@shared/components/tb-anchor.component';
import * as TbPopoverComponent from '@shared/components/popover.component';
@ -387,7 +386,6 @@ class ModulesMap implements IModulesMap {
'@shared/components/footer-fab-buttons.component': FooterFabButtonsComponent,
'@shared/components/fullscreen.directive': FullscreenDirective,
'@shared/components/circular-progress.directive': CircularProgressDirective,
'@shared/components/mat-chip-draggable.directive': MatChipDraggableDirective,
'@shared/components/hotkeys.directive': TbHotkeysDirective,
'@shared/components/tb-anchor.component': TbAnchorComponent,
'@shared/components/popover.component': TbPopoverComponent,

23
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss

@ -22,26 +22,3 @@
padding-right: 8px;
min-width: 160px;
}
:host ::ng-deep {
mat-form-field {
font-size: 16px;
width: 200px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
}
}

5
ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html

@ -16,14 +16,15 @@
-->
<mat-form-field [formGroup]="selectEntityInfoFormGroup" class="mat-block">
<input matInput type="text" placeholder="{{ alias }}"
<mat-label>{{ alias }}</mat-label>
<input matInput type="text"
#entityInfoInput
formControlName="entityInfo"
[required]="required"
[matAutocomplete]="entityInfoAutocomplete">
<button *ngIf="selectEntityInfoFormGroup.get('entityInfo').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>

2
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<form [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
<span fxFlex></span>

7
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -15,10 +15,9 @@
limitations under the License.
-->
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'"
[hideRequiredMarker]="!showLabel" [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label *ngIf="!showLabel"></mat-label>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}"
<mat-form-field [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label *ngIf="showLabel">{{ 'entity.entity-alias' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ !showLabel ? ('entity.entity-alias' | translate) : ''}}"
#entityAliasInput
formControlName="entityAlias"
(focusin)="onFocus()"

6
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss

@ -16,9 +16,3 @@
:host {
}
:host ::ng-deep {
.mat-form-field-infix {
border-top: none;
}
}

7
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 700px;">
<form [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ title | translate }}</h2>
<span fxFlex></span>
@ -32,7 +32,7 @@
<div class="tb-aliases-header" fxLayout="row" fxLayoutAlign="start center">
<span fxFlex="5"></span>
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<span class="tb-header-label" translate fxFlex="150px">alias.name</span>
<span class="tb-header-label" translate fxFlex fxFlex.gt-sm="200px">alias.name</span>
<span class="tb-header-label" translate fxFlex style="padding-left: 10px;">alias.entity-filter</span>
<span class="tb-header-label" translate fxFlex="120px" style="padding-left: 10px;">alias.resolve-multiple</span>
<span style="min-width: 80px;"></span>
@ -45,8 +45,7 @@
*ngFor="let entityAliasControl of entityAliasesFormArray().controls; let $index = index">
<span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4 tb-alias" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex="150px">
<mat-label></mat-label>
<mat-form-field class="mat-block" fxFlex fxFlex.gt-sm="200px" style="padding-top: 20px;">
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')">
{{ 'entity.alias-required' | translate }}

2
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.scss

@ -45,7 +45,7 @@
}
:host ::ng-deep {
.mat-dialog-content {
.mat-mdc-dialog-content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}

2
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2>{{ 'attribute.add' | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>

2
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>attribute.add-widget-to-dashboard</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>

13
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.scss

@ -13,16 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
mat-radio-button {
display: block;
margin-bottom: 16px;
.mat-radio-label {
width: 100%;
align-items: start;
.mat-radio-label-content {
width: 100%;
}
}
}
:host {
}

14
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -17,7 +17,7 @@
-->
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
@ -58,7 +58,7 @@
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
@ -78,7 +78,7 @@
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()">
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools" fxLayout.xs="row wrap">
<span class="tb-entity-table-info">
{{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ?
@ -104,7 +104,7 @@
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'widget'">
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [fxShow]="mode === 'widget'">
<div class="mat-toolbar-tools" fxLayoutGap="8px" fxLayout.xs="row wrap">
<div fxFlex fxLayout="row" fxLayoutAlign="start">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
@ -170,7 +170,7 @@
class="tb-value-cell"
(click)="editAttribute($event, attribute)">
<div fxLayout="row" style="align-items: center">
<span fxFlex>{{attribute.value | tbJson}}</span>
<span style="overflow: hidden;text-overflow: ellipsis;" fxFlex>{{attribute.value | tbJson}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</span>
@ -249,10 +249,10 @@
widgetBundleSet"
fxFlex fxLayoutAlign="center center"
style="display: flex;"
class="mat-headline">widgets-bundle.empty</span>
class="mat-headline-5">widgets-bundle.empty</span>
<span translate *ngIf="mode === 'widget' && !widgetBundleSet"
fxFlex fxLayoutAlign="center center"
style="display: flex;"
class="mat-headline">widget.select-widgets-bundle</span>
class="mat-headline-5">widget.select-widgets-bundle</span>
</div>
</div>

23
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -42,6 +42,9 @@
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
}
}
.tb-entity-table-info{
@ -75,26 +78,6 @@
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
mat-form-field.tb-attribute-scope {
font-size: 16px;
width: 200px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
}
mat-cell.tb-value-cell {
cursor: pointer;
mat-icon {

2
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html

@ -16,7 +16,7 @@
-->
<form class="mat-elevation-z1"
[formGroup]="attributeFormGroup" (ngSubmit)="update()" style="width: 400px; padding: 5px;">
[formGroup]="attributeFormGroup" (ngSubmit)="update()" style="padding: 5px;">
<fieldset [disabled]="isLoading$ | async">
<tb-value-input
formControlName="value"

2
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html

@ -18,7 +18,7 @@
<mat-toolbar color="primary">
<h2 translate>audit-log.audit-log-details</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
<button mat-icon-button
[mat-dialog-close]="false"
type="button">
<mat-icon class="material-icons">close</mat-icon>

2
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 900px;">
<form [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 1200px;">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>dashboard.update-image</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.scss

@ -18,7 +18,7 @@ $previewSize: 300px !default;
:host {
mat-form-field.rect-field {
max-width: 60px;
max-width: 80px;
}
.tb-image-preview {

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -157,7 +157,7 @@
<section *ngIf="!widgetEditMode" class="tb-dashboard-title"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
<h3 [fxShow]="!isEdit && displayTitle()">{{ translatedDashboardTitle }}</h3>
<mat-form-field [fxShow]="isEdit" class="mat-block">
<mat-form-field [fxShow]="isEdit" class="mat-block tb-appearance-transparent">
<mat-label translate [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">dashboard.title</mat-label>
<input matInput class="tb-dashboard-title"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}"
@ -285,7 +285,7 @@
</tb-widgets-bundle-search>
</div>
<div class="details-buttons" *ngIf="isAddingWidget">
<button mat-button mat-icon-button type="button"
<button mat-icon-button type="button"
*ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"

12
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -35,9 +35,15 @@ div.tb-dashboard-page {
position: relative;
padding-left: 20px;
max-height: 60px;
mat-form-field {
.mat-form-field-infix {
width: 100%;
.mat-mdc-form-field.mat-form-field-appearance-fill.tb-appearance-transparent {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-infix {
width: 100%;
padding-top: 20px;
.mat-mdc-floating-label {
top: 24px;
}
}
}
}
input.tb-dashboard-title {

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>{{settings ? 'dashboard.settings' : 'layout.settings'}}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>

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

Loading…
Cancel
Save