Browse Source

Merge remote-tracking branch 'ce/master' into Jave4to5-msa

pull/10686/head
Sergey Matvienko 2 years ago
parent
commit
187e76652b
  1. 2
      application/src/main/data/json/system/widget_types/alarms_table.json
  2. 25
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  3. 28
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  4. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  7. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  8. 60
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  9. 28
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
  11. 21
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  12. 6
      application/src/main/resources/thingsboard.yml
  13. 6
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  14. 27
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  15. 13
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  16. 2
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  17. 17
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  18. 14
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java
  19. 42
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheKey.java
  20. 33
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCaffeineCache.java
  21. 35
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerRedisCache.java
  22. 21
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheEvictEvent.java
  23. 42
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheKey.java
  24. 33
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCaffeineCache.java
  25. 35
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserRedisCache.java
  26. 5
      common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java
  27. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  28. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  29. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  30. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  31. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  32. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  33. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  34. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  35. 4
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  36. 12
      common/data/src/main/java/org/thingsboard/server/common/data/HasDefaultOption.java
  37. 3
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java
  38. 9
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
  39. 3
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java
  41. 19
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  42. 26
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
  43. 105
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  44. 21
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  45. 19
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  46. 21
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  47. 15
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  48. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  49. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java
  50. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java
  51. 5
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  52. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java
  53. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
  54. 20
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
  55. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  56. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
  57. 75
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  58. 2
      dao/src/main/resources/sql/schema-entities.sql
  59. 86
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java
  60. 43
      dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java
  61. 29
      dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java
  62. 6
      dao/src/test/resources/application-test.properties
  63. 6
      monitoring/src/main/resources/lwm2m/device_profile.json
  64. 4
      monitoring/src/main/resources/lwm2m/models/test-model.xml
  65. 2
      monitoring/src/main/resources/lwm2m/resource.json
  66. 3
      msa/tb-node/docker/Dockerfile
  67. 10
      netty-mqtt/pom.xml
  68. 18
      netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java
  69. 2
      netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/server/MqttServer.java
  70. 3
      netty-mqtt/src/test/resources/junit-platform.properties
  71. 14
      netty-mqtt/src/test/resources/logback-test.xml
  72. 2
      pom.xml
  73. 144
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java
  74. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNodeConfiguration.java
  75. 422
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  76. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNodeConfiguration.java
  77. 79
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java
  78. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeConfiguration.java
  79. 254
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
  80. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeConfiguration.java
  81. 110
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
  82. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
  83. 82
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java
  84. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeConfiguration.java
  85. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  86. 8
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  87. 332
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeTest.java
  88. 751
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java
  89. 605
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java
  90. 320
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java
  91. 37
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java
  92. 23
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
  93. 5
      transport/coap/pom.xml
  94. 5
      transport/http/pom.xml
  95. 5
      transport/lwm2m/pom.xml
  96. 5
      transport/mqtt/pom.xml
  97. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  98. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  99. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  100. 39
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

2
application/src/main/data/json/system/widget_types/alarms_table.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings",

25
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -14,6 +14,23 @@
-- limitations under the License.
--
-- UPDATE PUBLIC CUSTOMERS START
DO
$$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'customer' AND column_name = 'is_public'
) THEN
ALTER TABLE customer ADD COLUMN is_public boolean DEFAULT false;
UPDATE customer SET is_public = true WHERE title = 'Public';
END IF;
END;
$$;
-- UPDATE PUBLIC CUSTOMERS END
-- create new attribute_kv table schema
DO
$$
@ -90,13 +107,7 @@ BEGIN
SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old;
SELECT COUNT(*) INTO row_num FROM attribute_kv;
RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old;
IF row_num != 0 THEN
DROP TABLE IF EXISTS attribute_kv_old;
ELSE
RAISE EXCEPTION 'Table attribute_kv is empty';
END IF;
DROP TABLE IF EXISTS attribute_kv_old;
CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);
END IF;
EXCEPTION

28
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -72,12 +72,11 @@ public class ThingsboardSecurityConfiguration {
public static final String JWT_TOKEN_HEADER_PARAM_V2 = "Authorization";
public static final String JWT_TOKEN_QUERY_PARAM = "token";
public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"};
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
@ -153,7 +152,7 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT,
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
@ -210,18 +209,17 @@ public class ThingsboardSecurityConfiguration {
.exceptionHandling(config -> {})
.sessionManagement(config -> config.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(config -> config
.requestMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
.requestMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
.requestMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.requestMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.requestMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.requestMatchers(DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT).permitAll() // Device connectivity certificate (public)
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.requestMatchers(WS_ENTRY_POINT).permitAll() // Protected WebSocket API End-points
.requestMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()) // Protected API End-points
.formLogin(form -> form
.loginPage("/login").permitAll())
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included)
.requestMatchers(
DEVICE_API_ENTRY_POINT, // Device HTTP Transport API
FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point
PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point
TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point
MAIL_OAUTH2_PROCESSING_ENTRY_POINT, // Mail oauth2 code processing url
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT, // Device connectivity certificate (public)
WS_ENTRY_POINT).permitAll() // Protected WebSocket API End-points
.requestMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.anyRequest().permitAll())
.exceptionHandling(config -> config.accessDeniedHandler(restAccessDeniedHandler))
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -133,6 +133,8 @@ public class ThingsboardInstallService {
case "3.6.4":
log.info("Upgrading ThingsBoard from version 3.6.4 to 3.7.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.4");
dataUpdateService.updateData("3.6.4");
entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists();
systemDataLoaderService.updateDefaultNotificationConfigs(false);
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;

2
application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java

@ -21,4 +21,6 @@ public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createOrUpdateViewsAndFunctions() throws Exception;
void createCustomerTitleUniqueConstraintIfNotExists();
}

6
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -52,4 +52,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
log.info("Installing SQL DataBase schema views and functions: " + SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
executeQueryFromFile(SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
}
@Override
public void createCustomerTitleUniqueConstraintIfNotExists() {
executeQuery("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_title_unq_key') THEN " +
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;");
}
}

1
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -27,7 +27,6 @@ import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional;
import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.RESOURCE_INFO_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;

60
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -26,12 +26,15 @@ import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@ -42,6 +45,7 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@Service
@ -67,6 +71,13 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
DeviceConnectivityConfiguration connectivityConfiguration;
@Autowired
private CustomerDao customerDao;
@Autowired
private CustomerService customerService;
@Override
public void updateData(String fromVersion) throws Exception {
switch (fromVersion) {
@ -74,11 +85,60 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.info("Updating data from version 3.6.0 to 3.6.1 ...");
migrateDeviceConnectivity();
break;
case "3.6.4":
log.info("Updating data from version 3.6.4 to 3.7.0 ...");
updateCustomersWithTheSameTitle();
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
private void updateCustomersWithTheSameTitle() {
var customers = new ArrayList<Customer>();
new PageDataIterable<>(pageLink ->
customerDao.findCustomersWithTheSameTitle(pageLink), DEFAULT_PAGE_SIZE
).forEach(customers::add);
if (customers.isEmpty()) {
return;
}
var firstCustomer = customers.get(0);
var titleToDeduplicate = firstCustomer.getTitle();
var tenantIdToDeduplicate = firstCustomer.getTenantId();
int duplicateCounter = 1;
for (int i = 1; i < customers.size(); i++) {
var currentCustomer = customers.get(i);
if (currentCustomer.getTitle().equals(titleToDeduplicate) && currentCustomer.getTenantId().equals(tenantIdToDeduplicate)) {
duplicateCounter++;
String currentTitle = currentCustomer.getTitle();
String newTitle = currentTitle + " " + duplicateCounter;
try {
Optional<Customer> customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantIdToDeduplicate, newTitle);
if (customerOpt.isPresent()) {
// fallback logic: customer with title 'currentTitle + " " + duplicateCounter;' might be another duplicate.
newTitle = currentTitle + "_" + currentCustomer.getId();
}
} catch (Exception e) {
log.trace("Failed to find customer with title due to: ", e);
// fallback logic: customer with title 'currentTitle + " " + duplicateCounter;' might be another duplicate.
newTitle = currentTitle + "_" + currentCustomer.getId();
}
currentCustomer.setTitle(newTitle);
try {
customerService.saveCustomer(currentCustomer);
} catch (Exception e) {
log.error("[{}] Failed to update customer with id and title: {}, oldTitle: {}, due to: ",
currentCustomer.getTenantId(), newTitle, currentTitle, e);
}
continue;
}
titleToDeduplicate = currentCustomer.getTitle();
tenantIdToDeduplicate = currentCustomer.getTenantId();
duplicateCounter = 1;
}
}
private void migrateDeviceConnectivity() {
if (adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "connectivity") == null) {
AdminSettings connectivitySettings = new AdminSettings();

28
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java

@ -108,34 +108,40 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi
return entity;
}
@Override
public <E extends ExportableEntity<I>, I extends EntityId> E findDefaultEntityByTenantId(TenantId tenantId, EntityType entityType) {
ExportableEntityDao<I, E> dao = getExportableEntityDao(entityType);
if (dao == null) {
return null;
}
return dao.findDefaultEntityByTenantId(tenantId.getId());
}
@Override
public <E extends ExportableEntity<I>, I extends EntityId> PageData<E> findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) {
ExportableEntityDao<I, E> dao = getExportableEntityDao(entityType);
if (dao != null) {
return dao.findByTenantId(tenantId.getId(), pageLink);
} else {
return new PageData<>();
if (dao == null) {
return PageData.emptyPageData();
}
return dao.findByTenantId(tenantId.getId(), pageLink);
}
@Override
public <I extends EntityId> PageData<I> findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) {
ExportableEntityDao<I, ?> dao = getExportableEntityDao(entityType);
if (dao != null) {
return dao.findIdsByTenantId(tenantId.getId(), pageLink);
} else {
return new PageData<>();
if (dao == null) {
return PageData.emptyPageData();
}
return dao.findIdsByTenantId(tenantId.getId(), pageLink);
}
@Override
public <I extends EntityId> I getExternalIdByInternal(I internalId) {
ExportableEntityDao<I, ?> dao = getExportableEntityDao(internalId.getEntityType());
if (dao != null) {
return dao.getExternalIdByInternal(internalId);
} else {
if (dao == null) {
return null;
}
return dao.getExternalIdByInternal(internalId);
}
private boolean belongsToTenant(HasId<? extends EntityId> entity, TenantId tenantId) {

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java

@ -33,6 +33,8 @@ public interface ExportableEntitiesService {
<E extends ExportableEntity<I>, I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name);
<E extends ExportableEntity<I>, I extends EntityId> E findDefaultEntityByTenantId(TenantId tenantId, EntityType entityType);
<E extends ExportableEntity<I>, I extends EntityId> PageData<E> findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);
<I extends EntityId> PageData<I> findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);

21
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -27,6 +27,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -73,7 +74,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Autowired
@Lazy
private ExportableEntitiesService exportableEntitiesService;
private ExportableEntitiesService entitiesService;
@Autowired
private RelationService relationService;
@Autowired
@ -274,22 +275,30 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@SuppressWarnings("unchecked")
protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
return (E) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
.or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entity.getId())))
return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
.or(() -> Optional.ofNullable(entitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entity.getId())))
.or(() -> {
if (ctx.isFindExistingByName()) {
return Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndName(ctx.getTenantId(), getEntityType(), entity.getName()));
return Optional.ofNullable(entitiesService.findEntityByTenantIdAndName(ctx.getTenantId(), getEntityType(), entity.getName()));
} else {
return Optional.empty();
}
})
.or(() -> {
if (entity instanceof HasDefaultOption hasDefaultOption) {
if (hasDefaultOption.isDefault()) {
return Optional.ofNullable(entitiesService.findDefaultEntityByTenantId(ctx.getTenantId(), getEntityType()));
}
}
return Optional.empty();
})
.orElse(null);
}
@SuppressWarnings("unchecked")
private <ID extends EntityId> HasId<ID> findInternalEntity(TenantId tenantId, ID externalId) {
return (HasId<ID>) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(tenantId, externalId))
.or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(tenantId, externalId)))
return (HasId<ID>) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(tenantId, externalId))
.or(() -> Optional.ofNullable(entitiesService.findEntityByTenantIdAndId(tenantId, externalId)))
.orElseThrow(() -> new MissingEntityException(externalId));
}

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

@ -505,6 +505,12 @@ cache:
assets:
timeToLiveInMinutes: "${CACHE_SPECS_ASSETS_TTL:1440}" # Asset cache TTL
maxSize: "${CACHE_SPECS_ASSETS_MAX_SIZE:10000}" # 0 means the cache is disabled
customers:
timeToLiveInMinutes: "${CACHE_SPECS_CUSTOMERS_TTL:1440}" # Customer cache TTL
maxSize: "${CACHE_SPECS_CUSTOMERS_MAX_SIZE:10000}" # 0 means the cache is disabled
users:
timeToLiveInMinutes: "${CACHE_SPECS_USERS_TTL:1440}" # User cache TTL
maxSize: "${CACHE_SPECS_USERS_MAX_SIZE:10000}" # 0 means the cache is disabled
entityViews:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_VIEWS_TTL:1440}" # Entity view cache TTL
maxSize: "${CACHE_SPECS_ENTITY_VIEWS_MAX_SIZE:10000}" # 0 means the cache is disabled

6
application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java

@ -167,4 +167,10 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.message",
is("Password must be no more than 72 characters in length.")));
}
@Test
public void testGetPageWithoutRedirect() throws Exception {
doGet("/login").andExpect(status().isOk());
doGet("/home").andExpect(status().isOk());
}
}

27
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -352,7 +352,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
}
@Test
public void testFindCustomersByTitle() throws Exception {
public void testFindCustomersWithTitleAsTextSearch() throws Exception {
TenantId tenantId = savedTenant.getId();
String title1 = "Customer title 1";
@ -424,6 +424,31 @@ public class CustomerControllerTest extends AbstractControllerTest {
Assert.assertEquals(0, pageData.getData().size());
}
@Test
public void testFindCustomerByTitle() throws Exception {
Customer customer = new Customer();
customer.setTitle("My customer");
Mockito.reset(tbClusterService, auditLogService);
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
testNotifyEntityAllOneTime(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), savedCustomer.getTenantId(),
new CustomerId(CustomerId.NULL_UUID), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED);
Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
Customer foundCustomer = doGet("/api/tenant/customers?customerTitle=" + savedCustomer.getTitle(), Customer.class);
Assert.assertEquals(foundCustomer, savedCustomer);
doDelete("/api/customer/" + savedCustomer.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testDeleteCustomerWithDeleteRelationsOk() throws Exception {
CustomerId customerId = createCustomer("Customer for Test WithRelationsOk").getId();

13
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -275,10 +275,9 @@ public class EntityViewControllerTest extends AbstractControllerTest {
}
@Test
public void testAssignAndUnAssignedEntityViewToCustomer() throws Exception {
public void testAssignAndUnassignEntityViewToCustomer() throws Exception {
EntityView view = getNewSavedEntityView("Test entity view");
Customer savedCustomer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class);
view.setCustomerId(savedCustomer.getId());
Mockito.reset(tbClusterService, auditLogService);
@ -304,17 +303,17 @@ public class EntityViewControllerTest extends AbstractControllerTest {
ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED,
foundView.getId().getId().toString(), foundView.getCustomerId().getId().toString(), savedCustomer.getTitle());
EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId());
EntityView unassignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unassignedView.getCustomerId().getId());
foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId());
testBroadcastEntityStateChangeEventTime(foundView.getId(), foundView.getTenantId(), 1);
testNotifyAssignUnassignEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(),
tenantId, savedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
testNotifyAssignUnassignEntityAllOneTime(unassignedView, unassignedView.getId(), unassignedView.getId(),
tenantId, savedCustomer.getId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED,
assignedView.getId().getId().toString(), savedView.getCustomerId().getId().toString(), savedCustomer.getTitle());
assignedView.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle());
}
@Test

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

@ -279,7 +279,7 @@ public class UserControllerTest extends AbstractControllerTest {
user.setTenantId(tenantId);
user.setEmail(TENANT_ADMIN_EMAIL);
String msgError = "User with email '" + TENANT_ADMIN_EMAIL + "' already present in database";
String msgError = "User with email '" + TENANT_ADMIN_EMAIL + "' already present in database!";
doPost("/api/user", user)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));

17
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -532,20 +532,19 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportDeviceProfile_betweenTenants_findExistingByName() throws Exception {
public void testExportImportDefaultDeviceProfile_betweenTenants_findExisting() throws Exception {
DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId1);
defaultDeviceProfile.setName("non-default-name");
deviceProfileService.saveDeviceProfile(defaultDeviceProfile);
EntityExportData<DeviceProfile> deviceProfileExportData = exportEntity(tenantAdmin1, defaultDeviceProfile.getId());
assertThatThrownBy(() -> {
importEntity(tenantAdmin2, deviceProfileExportData, EntityImportSettings.builder()
.findExistingByName(false)
.build());
}).hasMessageContaining("default device profile is present");
importEntity(tenantAdmin2, deviceProfileExportData, EntityImportSettings.builder()
.findExistingByName(true)
.findExistingByName(false)
.build());
checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, deviceProfileService.findDefaultDeviceProfile(tenantId2));
DeviceProfile importedDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId2);
assertThat(importedDeviceProfile.isDefault()).isTrue();
assertThat(importedDeviceProfile.getName()).isEqualTo(defaultDeviceProfile.getName());
checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, importedDeviceProfile);
}

14
netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/IntegrationTestSuite.java → common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java

@ -13,15 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.mqtt.integration;
package org.thingsboard.server.cache.customer;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.common.data.id.TenantId;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.mqtt.integration.*Test",
})
public class IntegrationTestSuite {
}
public record CustomerCacheEvictEvent(TenantId tenantId, String newTitle, String oldTitle) {
}

42
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheKey.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2024 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.cache.customer;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode
@RequiredArgsConstructor
public class CustomerCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 5706958428811356925L;
@NonNull
private final TenantId tenantId;
private final String title;
@Override
public String toString() {
return tenantId.getId() + "_" + title;
}
}

33
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 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.cache.customer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Customer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("CustomerCache")
public class CustomerCaffeineCache extends CaffeineTbTransactionalCache<CustomerCacheKey, Customer> {
public CustomerCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.CUSTOMER_CACHE);
}
}

35
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 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.cache.customer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Customer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("CustomerCache")
public class CustomerRedisCache extends RedisTbTransactionalCache<CustomerCacheKey, Customer> {
public CustomerRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.CUSTOMER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(Customer.class));
}
}

21
common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheEvictEvent.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2024 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.cache.user;
import org.thingsboard.server.common.data.id.TenantId;
public record UserCacheEvictEvent(TenantId tenantId, String newEmail, String oldEmail) {
}

42
common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheKey.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2024 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.cache.user;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode
@RequiredArgsConstructor
public class UserCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 7357353074893750678L;
@NonNull
private final TenantId tenantId;
private final String email;
@Override
public String toString() {
return tenantId.getId() + "_" + email;
}
}

33
common/cache/src/main/java/org/thingsboard/server/cache/user/UserCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 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.cache.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.User;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UserCache")
public class UserCaffeineCache extends CaffeineTbTransactionalCache<UserCacheKey, User> {
public UserCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USER_CACHE);
}
}

35
common/cache/src/main/java/org/thingsboard/server/cache/user/UserRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 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.cache.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.User;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UserCache")
public class UserRedisCache extends RedisTbTransactionalCache<UserCacheKey, User> {
public UserRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(User.class));
}
}

5
common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java

@ -26,6 +26,7 @@ import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.thingsboard.server.common.data.CacheConstants;
import static org.assertj.core.api.Assertions.assertThat;
@ -53,8 +54,8 @@ public class CacheSpecsMapTest {
@Test
public void givenCacheConfig_whenCacheManagerReady_thenVerifyExistedCachesWithNoTransactionAwareCacheDecorator() {
// We no longer use built-in transaction support for the caches, because we have our own cache cleanup and transaction logic that implements CAS.
assertThat(cacheManager.getCache("relations")).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache("devices")).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache(CacheConstants.RELATIONS_CACHE)).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache(CacheConstants.DEVICE_CACHE)).isInstanceOf(CaffeineCache.class);
}
@Test

2
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -41,6 +41,8 @@ public interface AssetService extends EntityDaoService {
Asset findAssetByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<Asset> findAssetByTenantIdAndNameAsync(TenantId tenantId, String name);
Asset saveAsset(Asset asset, boolean doValidate);
Asset saveAsset(Asset asset);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java

@ -31,6 +31,8 @@ public interface CustomerService extends EntityDaoService {
Optional<Customer> findCustomerByTenantIdAndTitle(TenantId tenantId, String title);
ListenableFuture<Optional<Customer>> findCustomerByTenantIdAndTitleAsync(TenantId tenantId, String title);
ListenableFuture<Customer> findCustomerByIdAsync(TenantId tenantId, CustomerId customerId);
Customer saveCustomer(Customer customer);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java

@ -72,6 +72,8 @@ public interface DashboardService extends EntityDaoService {
DashboardInfo findFirstDashboardInfoByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<DashboardInfo> findFirstDashboardInfoByTenantIdAndNameAsync(TenantId tenantId, String name);
List<Dashboard> findTenantDashboardsByTitle(TenantId tenantId, String title);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -49,6 +49,8 @@ public interface DeviceService extends EntityDaoService {
Device findDeviceByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<Device> findDeviceByTenantIdAndNameAsync(TenantId tenantId, String name);
Device saveDevice(Device device);
Device saveDevice(Device device, boolean doValidate);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java

@ -42,6 +42,8 @@ public interface EdgeService extends EntityDaoService {
Edge findEdgeByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<Edge> findEdgeByTenantIdAndNameAsync(TenantId tenantId, String name);
Optional<Edge> findEdgeByRoutingKey(TenantId tenantId, String routingKey);
Edge saveEdge(Edge edge);

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

@ -54,6 +54,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<EntityView> findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name);
PageData<EntityView> findEntityViewByTenantId(TenantId tenantId, PageLink pageLink);
PageData<EntityViewInfo> findEntityViewInfosByTenantId(TenantId tenantId, PageLink pageLink);

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

@ -41,6 +41,8 @@ public interface UserService extends EntityDaoService {
User findUserByTenantIdAndEmail(TenantId tenantId, String email);
ListenableFuture<User> findUserByTenantIdAndEmailAsync(TenantId tenantId, String email);
User saveUser(TenantId tenantId, User user);
UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId);

2
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -21,6 +21,8 @@ public class CacheConstants {
public static final String DEVICE_CACHE = "devices";
public static final String SESSIONS_CACHE = "sessions";
public static final String ASSET_CACHE = "assets";
public static final String CUSTOMER_CACHE = "customers";
public static final String USER_CACHE = "users";
public static final String ENTITY_VIEW_CACHE = "entityViews";
public static final String EDGE_CACHE = "edges";
public static final String CLAIM_DEVICES_CACHE = "claimDevices";

4
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@ -33,7 +34,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import jakarta.validation.Valid;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -42,7 +42,7 @@ import java.io.IOException;
@ToString(exclude = {"image", "profileDataBytes"})
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity<DeviceProfileId>, HasImage {
public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity<DeviceProfileId>, HasImage, HasDefaultOption {
private static final long serialVersionUID = 6998485460273302018L;

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityContainer.java → common/data/src/main/java/org/thingsboard/server/common/data/HasDefaultOption.java

@ -13,16 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
package org.thingsboard.server.common.data;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
public interface HasDefaultOption {
@Data
public class EntityContainer {
private EntityId entityId;
private EntityType entityType;
boolean isDefault();
}

3
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java

@ -22,6 +22,7 @@ import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
@ -38,7 +39,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ToString(exclude = {"image"})
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class AssetProfile extends BaseData<AssetProfileId> implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity<AssetProfileId>, HasImage {
public class AssetProfile extends BaseData<AssetProfileId> implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity<AssetProfileId>, HasImage, HasDefaultOption {
private static final long serialVersionUID = 6998485460273302018L;

9
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java

@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.RuleChainId;
@ -35,7 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class RuleChain extends BaseDataWithAdditionalInfo<RuleChainId> implements HasName, HasTenantId, ExportableEntity<RuleChainId> {
public class RuleChain extends BaseDataWithAdditionalInfo<RuleChainId> implements HasName, HasTenantId, ExportableEntity<RuleChainId>, HasDefaultOption {
private static final long serialVersionUID = -5656679015121935465L;
@ -108,4 +109,10 @@ public class RuleChain extends BaseDataWithAdditionalInfo<RuleChainId> implement
setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes);
}
@JsonIgnore
@Override
public boolean isDefault() {
return root && type == RuleChainType.CORE;
}
}

3
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java

@ -143,8 +143,9 @@ public class OtaPackageTransportResource extends AbstractCoapTransportResource {
response.setPayload(data);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
int blockNum = exchange.getRequestOptions().getBlock2().getNum();
boolean lastFlag = data.length <= chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
response.getOptions().setBlock2(chunkSize, lastFlag, blockNum);
}
transportContext.getExecutor().submit(() -> exchange.respond(response));
}

2
dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java

@ -36,4 +36,6 @@ public interface ExportableEntityDao<I extends EntityId, T extends ExportableEnt
I getExternalIdByInternal(I internalId);
default T findDefaultEntityByTenantId(UUID tenantId) { throw new UnsupportedOperationException(); }
}

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

@ -53,6 +53,7 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.ArrayList;
import java.util.Collections;
@ -88,6 +89,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Autowired
private EntityCountService countService;
@Autowired
private JpaExecutorService executor;
@TransactionalEventListener(classes = AssetCacheEvictEvent.class)
@Override
public void handleEvictEvent(AssetCacheEvictEvent event) {
@ -115,7 +119,7 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public ListenableFuture<Asset> findAssetByIdAsync(TenantId tenantId, AssetId assetId) {
log.trace("Executing findAssetById [{}]", assetId);
log.trace("Executing findAssetByIdAsync [{}]", assetId);
validateId(assetId, id -> INCORRECT_ASSET_ID + id);
return assetDao.findByIdAsync(tenantId, assetId.getId());
}
@ -129,6 +133,13 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
.orElse(null), true);
}
@Override
public ListenableFuture<Asset> findAssetByTenantIdAndNameAsync(TenantId tenantId, String name) {
log.trace("Executing findAssetByTenantIdAndNameAsync [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return executor.submit(() -> findAssetByTenantIdAndName(tenantId, name));
}
@Override
public Asset saveAsset(Asset asset) {
return saveAsset(asset, true);
@ -184,6 +195,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId) {
Asset asset = findAssetById(tenantId, assetId);
if (customerId.equals(asset.getCustomerId())) {
return asset;
}
asset.setCustomerId(customerId);
return saveAsset(asset);
}
@ -191,6 +205,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId) {
Asset asset = findAssetById(tenantId, assetId);
if (asset.getCustomerId() == null) {
return asset;
}
asset.setCustomerId(null);
return saveAsset(asset);
}

26
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java

@ -45,17 +45,37 @@ public interface CustomerDao extends Dao<Customer>, TenantEntityDao, ExportableE
*
* @param tenantId the tenant id
* @param pageLink the page link
* @return the list of customer objects
* @return the page of customer objects
*/
PageData<Customer> findCustomersByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find customers by tenantId and customer title.
* Find customer by tenantId and customer title.
*
* @param tenantId the tenantId
* @param title the customer title
* @return the optional customer object
*/
Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title);
Optional<Customer> findCustomerByTenantIdAndTitle(UUID tenantId, String title);
/**
* Find public customer by tenantId.
*
* @param tenantId the tenantId
* @return the optional public customer object
*/
Optional<Customer> findPublicCustomerByTenantId(UUID tenantId);
/**
* Find customers with the same title within the same tenant.
* This method was created to upgrade customers with the same title before creation of
* CONSTRAINT customer_title_unq_key UNIQUE (tenant_id, title).
* If constraint already exists this method will return nothing.
*
* @param pageLink the page link
* @return the page of customer objects
*/
PageData<Customer> findCustomersWithTheSameTitle(PageLink pageLink);
}

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

@ -15,15 +15,20 @@
*/
package org.thingsboard.server.dao.customer;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.customer.CustomerCacheEvictEvent;
import org.thingsboard.server.cache.customer.CustomerCacheKey;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -33,28 +38,34 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("CustomerDaoService")
@Slf4j
public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCacheKey, Customer, CustomerCacheEvictEvent> implements CustomerService {
public static final String PUBLIC_CUSTOMER_TITLE = "Public";
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final JsonNode PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON = JacksonUtil.toJsonNode("{\"isPublic\":true}");
public static final String CUSTOMER_UNIQUE_TITLE_EX_MSG = "Customer with such title already exists!";
@Autowired
private CustomerDao customerDao;
@ -81,6 +92,20 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
@Autowired
private EntityCountService countService;
@Autowired
private JpaExecutorService executor;
@TransactionalEventListener(classes = CustomerCacheEvictEvent.class)
@Override
public void handleEvictEvent(CustomerCacheEvictEvent event) {
List<CustomerCacheKey> keys = new ArrayList<>(2);
keys.add(new CustomerCacheKey(event.tenantId(), event.newTitle()));
if (StringUtils.isNotEmpty(event.oldTitle()) && !event.oldTitle().equals(event.newTitle())) {
keys.add(new CustomerCacheKey(event.tenantId(), event.oldTitle()));
}
cache.evict(keys);
}
@Override
public Customer findCustomerById(TenantId tenantId, CustomerId customerId) {
log.trace("Executing findCustomerById [{}]", customerId);
@ -92,7 +117,16 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
public Optional<Customer> findCustomerByTenantIdAndTitle(TenantId tenantId, String title) {
log.trace("Executing findCustomerByTenantIdAndTitle [{}] [{}]", tenantId, title);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), title);
return Optional.ofNullable(cache.getAndPutInTransaction(new CustomerCacheKey(tenantId, title),
() -> customerDao.findCustomerByTenantIdAndTitle(tenantId.getId(), title)
.orElse(null), true));
}
@Override
public ListenableFuture<Optional<Customer>> findCustomerByTenantIdAndTitleAsync(TenantId tenantId, String title) {
log.trace("Executing findCustomerByTenantIdAndTitleAsync [{}] [{}]", tenantId, title);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return executor.submit(() -> findCustomerByTenantIdAndTitle(tenantId, title));
}
@Override
@ -103,23 +137,40 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
}
@Override
@Transactional
public Customer saveCustomer(Customer customer) {
return saveCustomer(customer, true);
}
private Customer saveCustomer(Customer customer, boolean doValidate) {
log.trace("Executing saveCustomer [{}]", customer);
customerValidator.validate(customer, Customer::getTenantId);
String oldCustomerTitle = null;
if (doValidate) {
Customer oldCustomer = customerValidator.validate(customer, Customer::getTenantId);
if (oldCustomer != null) {
oldCustomerTitle = oldCustomer.getTitle();
}
}
var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle);
try {
Customer savedCustomer = customerDao.save(customer.getTenantId(), customer);
dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId());
Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer);
if (!savedCustomer.isPublic()) {
dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId());
}
if (customer.getId() == null) {
countService.publishCountEntityEvictEvent(savedCustomer.getTenantId(), EntityType.CUSTOMER);
}
publishEvictEvent(evictEvent);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCustomer.getTenantId())
.entityId(savedCustomer.getId()).created(customer.getId() == null).build());
return savedCustomer;
} catch (Exception e) {
checkConstraintViolation(e, "customer_external_id_unq_key", "Customer with such external id already exists!");
handleEvictEvent(evictEvent);
checkConstraintViolation(e,
"customer_title_unq_key", CUSTOMER_UNIQUE_TITLE_EX_MSG,
"customer_external_id_unq_key", "Customer with such external id already exists!");
throw e;
}
}
@Override
@ -152,27 +203,35 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
customerDao.removeById(tenantId, customerId.getId());
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(customerId).build());
publishEvictEvent(new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), null));
}
@Override
public Customer findOrCreatePublicCustomer(TenantId tenantId) {
log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, id -> INCORRECT_CUSTOMER_ID + id);
Optional<Customer> publicCustomerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get();
} else {
Customer publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try {
publicCustomer.setAdditionalInfo(JacksonUtil.toJsonNode("{ \"isPublic\": true }"));
} catch (IllegalArgumentException e) {
throw new IncorrectParameterException("Unable to create public customer.", e);
}
var publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try {
publicCustomer.setAdditionalInfo(PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON);
} catch (IllegalArgumentException e) {
throw new IncorrectParameterException("Unable to create public customer.", e);
}
try {
return saveCustomer(publicCustomer, false);
} catch (DataValidationException e) {
if (CUSTOMER_UNIQUE_TITLE_EX_MSG.equals(e.getMessage())) {
publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get();
}
}
Customer savedCustomer = customerDao.save(tenantId, publicCustomer);
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
return savedCustomer;
throw new RuntimeException("Failed to create public customer.", e);
}
}
@ -196,8 +255,8 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
deleteCustomersByTenantId(tenantId);
}
private PaginatedRemover<TenantId, Customer> customersByTenantRemover =
new PaginatedRemover<TenantId, Customer>() {
private final PaginatedRemover<TenantId, Customer> customersByTenantRemover =
new PaginatedRemover<>() {
@Override
protected PageData<Customer> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {

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

@ -54,6 +54,7 @@ import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.List;
import java.util.Optional;
@ -94,6 +95,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private JpaExecutorService executor;
protected void publishEvictEvent(DashboardTitleEvictEvent event) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
eventPublisher.publishEvent(event);
@ -171,8 +175,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
}
}
@Override
public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId) {
Dashboard dashboard = findDashboardById(tenantId, dashboardId);
@ -216,12 +218,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
}
}
private Dashboard updateAssignedCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer) {
private void updateAssignedCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer) {
Dashboard dashboard = findDashboardById(tenantId, dashboardId);
if (dashboard.updateAssignedCustomer(customer)) {
return saveDashboard(dashboard);
} else {
return dashboard;
saveDashboard(dashboard);
}
}
@ -369,9 +369,18 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Override
public DashboardInfo findFirstDashboardInfoByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findFirstDashboardInfoByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return dashboardInfoDao.findFirstByTenantIdAndName(tenantId.getId(), name);
}
@Override
public ListenableFuture<DashboardInfo> findFirstDashboardInfoByTenantIdAndNameAsync(TenantId tenantId, String name) {
log.trace("Executing findFirstDashboardInfoByTenantIdAndNameAsync [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return executor.submit(() -> findFirstDashboardInfoByTenantIdAndName(tenantId, name));
}
@Override
public List<Dashboard> findTenantDashboardsByTitle(TenantId tenantId, String title) {
return dashboardDao.findByTenantIdAndTitle(tenantId.getId(), title);

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

@ -78,6 +78,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.ArrayList;
@ -123,6 +124,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Autowired
private EntityCountService countService;
@Autowired
private JpaExecutorService executor;
@Override
public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceInfoById [{}]", deviceId);
@ -145,7 +149,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Override
public ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
log.trace("Executing findDeviceByIdAsync [{}]", deviceId);
validateId(deviceId, id -> INCORRECT_DEVICE_ID + id);
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return deviceDao.findByIdAsync(tenantId, deviceId.getId());
@ -162,6 +166,13 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
() -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true);
}
@Override
public ListenableFuture<Device> findDeviceByTenantIdAndNameAsync(TenantId tenantId, String name) {
log.trace("Executing findDeviceByTenantIdAndNameAsync [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return executor.submit(() -> findDeviceByTenantIdAndName(tenantId, name));
}
@Transactional
@Override
public Device saveDeviceWithAccessToken(Device device, String accessToken) {
@ -306,6 +317,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Override
public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) {
Device device = findDeviceById(tenantId, deviceId);
if (customerId.equals(device.getCustomerId())) {
return device;
}
device.setCustomerId(customerId);
return saveDevice(device);
}
@ -314,6 +328,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Override
public Device unassignDeviceFromCustomer(TenantId tenantId, DeviceId deviceId) {
Device device = findDeviceById(tenantId, deviceId);
if (device.getCustomerId() == null) {
return device;
}
device.setCustomerId(null);
return saveDevice(device);
}

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

@ -69,6 +69,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
@ -118,6 +119,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Autowired
private DataValidator<Edge> edgeValidator;
@Autowired
private JpaExecutorService executor;
@Value("${edges.enabled}")
@Getter
private boolean edgesEnabled;
@ -151,7 +155,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Override
public ListenableFuture<Edge> findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId) {
log.trace("Executing findEdgeById [{}]", edgeId);
log.trace("Executing findEdgeByIdAsync [{}]", edgeId);
validateId(edgeId, id -> INCORRECT_EDGE_ID + id);
return edgeDao.findByIdAsync(tenantId, edgeId.getId());
}
@ -165,6 +169,13 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
.orElse(null), true);
}
@Override
public ListenableFuture<Edge> findEdgeByTenantIdAndNameAsync(TenantId tenantId, String name) {
log.trace("Executing findEdgeByTenantIdAndNameAsync [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return executor.submit(() -> findEdgeByTenantIdAndName(tenantId, name));
}
@Override
public Optional<Edge> findEdgeByRoutingKey(TenantId tenantId, String routingKey) {
log.trace("Executing findEdgeByRoutingKey [{}]", routingKey);
@ -199,6 +210,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId) {
log.trace("[{}] Executing assignEdgeToCustomer [{}][{}]", tenantId, edgeId, customerId);
Edge edge = findEdgeById(tenantId, edgeId);
if (customerId.equals(edge.getCustomerId())) {
return edge;
}
edge.setCustomerId(customerId);
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(edgeId)
.body(JacksonUtil.toString(customerId)).actionType(ActionType.ASSIGNED_TO_CUSTOMER).build());
@ -209,7 +223,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
public Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId) {
log.trace("[{}] Executing unassignEdgeFromCustomer [{}]", tenantId, edgeId);
Edge edge = findEdgeById(tenantId, edgeId);
var customerId = edge.getCustomerId();
CustomerId customerId = edge.getCustomerId();
if (customerId == null) {
return edge;
}
edge.setCustomerId(null);
Edge result = saveEdge(edge);
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(edgeId)

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

@ -131,6 +131,9 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
@Override
public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
if (customerId.equals(entityView.getCustomerId())) {
return entityView;
}
entityView.setCustomerId(customerId);
return saveEntityView(entityView);
}
@ -138,6 +141,9 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
@Override
public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
if (entityView.getCustomerId() == null) {
return entityView;
}
entityView.setCustomerId(null);
return saveEntityView(entityView);
}
@ -181,6 +187,13 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
}
@Override
public ListenableFuture<EntityView> findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name) {
log.trace("Executing findEntityViewByTenantIdAndNameAsync [{}][{}]", tenantId, name);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return service.submit(() -> findEntityViewByTenantIdAndName(tenantId, name));
}
@Override
public PageData<EntityView> findEntityViewByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@ -290,7 +303,7 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return entityViewDao.findByIdAsync(tenantId, entityViewId.getId());
}

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -135,6 +135,7 @@ public class ModelConstants {
public static final String CUSTOMER_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String CUSTOMER_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String CUSTOMER_IS_PUBLIC_PROPERTY = "is_public";
/**
* Device constants.

4
dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java

@ -67,6 +67,9 @@ public final class CustomerEntity extends BaseSqlEntity<Customer> {
@Column(name = ModelConstants.EMAIL_PROPERTY)
private String email;
@Column(name = ModelConstants.CUSTOMER_IS_PUBLIC_PROPERTY)
private boolean isPublic;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
@ -94,6 +97,7 @@ public final class CustomerEntity extends BaseSqlEntity<Customer> {
this.phone = customer.getPhone();
this.email = customer.getEmail();
this.additionalInfo = customer.getAdditionalInfo();
this.isPublic = customer.isPublic();
if (customer.getExternalId() != null) {
this.externalId = customer.getExternalId().getId();
}

21
dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java

@ -27,8 +27,6 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Optional;
@Component
public class CustomerDataValidator extends DataValidator<Customer> {
@ -41,24 +39,15 @@ public class CustomerDataValidator extends DataValidator<Customer> {
@Override
protected void validateCreate(TenantId tenantId, Customer customer) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.CUSTOMER);
customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
c -> {
throw new DataValidationException("Customer with such title already exists!");
}
);
}
@Override
protected Customer validateUpdate(TenantId tenantId, Customer customer) {
Optional<Customer> customerOpt = customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle());
customerOpt.ifPresent(
c -> {
if (!c.getId().equals(customer.getId())) {
throw new DataValidationException("Customer with such title already exists!");
}
}
);
return customerOpt.orElse(null);
Customer old = customerDao.findById(customer.getTenantId(), customer.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing customer!");
}
return old;
}
@Override

5
dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java

@ -122,11 +122,6 @@ public class UserDataValidator extends DataValidator<User> {
break;
}
User existentUserWithEmail = userService.findUserByEmail(tenantId, user.getEmail());
if (existentUserWithEmail != null && !isSameData(existentUserWithEmail, user)) {
throw new DataValidationException("User with email '" + user.getEmail() + "' "
+ " already present in database!");
}
if (!tenantId.getId().equals(ModelConstants.NULL_UUID)) {
if (!tenantService.tenantExists(user.getTenantId())) {
throw new DataValidationException("User is referencing to non-existent tenant!");

5
dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java

@ -132,6 +132,11 @@ public class JpaAssetProfileDao extends JpaAbstractDao<AssetProfileEntity, Asset
.map(AssetProfileId::new).orElse(null);
}
@Override
public AssetProfile findDefaultEntityByTenantId(UUID tenantId) {
return findDefaultAssetProfile(TenantId.fromUUID(tenantId));
}
@Override
public List<AssetProfileInfo> findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) {
return assetProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit));

12
dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java

@ -38,9 +38,21 @@ public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID>,
CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title);
@Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " +
"AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true)
CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId);
Long countByTenantId(UUID tenantId);
@Query("SELECT externalId FROM CustomerEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query(value = "SELECT c.* FROM customer c " +
"INNER JOIN (SELECT tenant_id, title FROM customer GROUP BY tenant_id, title HAVING COUNT(title) > 1) dc " +
"ON c.tenant_id = dc.tenant_id AND c.title = dc.title " +
"ORDER BY c.tenant_id, c.title, c.id",
nativeQuery = true)
Page<CustomerEntity> findCustomersWithTheSameTitle(Pageable pageable);
}

20
dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java

@ -30,7 +30,6 @@ import org.thingsboard.server.dao.model.sql.CustomerEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -63,9 +62,13 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
}
@Override
public Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
Customer customer = DaoUtil.getData(customerRepository.findByTenantIdAndTitle(tenantId, title));
return Optional.ofNullable(customer);
public Optional<Customer> findCustomerByTenantIdAndTitle(UUID tenantId, String title) {
return Optional.ofNullable(DaoUtil.getData(customerRepository.findByTenantIdAndTitle(tenantId, title)));
}
@Override
public Optional<Customer> findPublicCustomerByTenantId(UUID tenantId) {
return Optional.ofNullable(DaoUtil.getData(customerRepository.findPublicCustomerByTenantId(tenantId)));
}
@Override
@ -80,7 +83,7 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
@Override
public Customer findByTenantIdAndName(UUID tenantId, String name) {
return findCustomersByTenantIdAndTitle(tenantId, name).orElse(null);
return findCustomerByTenantIdAndTitle(tenantId, name).orElse(null);
}
@Override
@ -94,6 +97,13 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
.map(CustomerId::new).orElse(null);
}
@Override
public PageData<Customer> findCustomersWithTheSameTitle(PageLink pageLink) {
return DaoUtil.toPageData(
customerRepository.findCustomersWithTheSameTitle(DaoUtil.toPageable(pageLink))
);
}
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;

5
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -150,6 +150,11 @@ public class JpaDeviceProfileDao extends JpaAbstractDao<DeviceProfileEntity, Dev
.map(DeviceProfileId::new).orElse(null);
}
@Override
public DeviceProfile findDefaultEntityByTenantId(UUID tenantId) {
return findDefaultDeviceProfile(TenantId.fromUUID(tenantId));
}
@Override
public List<DeviceProfileInfo> findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) {
return deviceProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit));

5
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java

@ -129,6 +129,11 @@ public class JpaRuleChainDao extends JpaAbstractDao<RuleChainEntity, RuleChain>
.map(RuleChainId::new).orElse(null);
}
@Override
public RuleChain findDefaultEntityByTenantId(UUID tenantId) {
return findRootRuleChainByTenantIdAndType(tenantId, RuleChainType.CORE);
}
@Override
public EntityType getEntityType() {
return EntityType.RULE_CHAIN;

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

@ -27,8 +27,12 @@ import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.user.UserCacheEvictEvent;
import org.thingsboard.server.cache.user.UserCacheKey;
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.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -47,7 +51,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
@ -55,7 +59,9 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -71,7 +77,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service("UserDaoService")
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends AbstractEntityService implements UserService {
public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, User, UserCacheEvictEvent> implements UserService {
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
@ -96,6 +102,18 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
private final EntityCountService countService;
private final JpaExecutorService executor;
@TransactionalEventListener(classes = UserCacheEvictEvent.class)
@Override
public void handleEvictEvent(UserCacheEvictEvent event) {
List<UserCacheKey> keys = new ArrayList<>(2);
keys.add(new UserCacheKey(event.tenantId(), event.newEmail()));
if (StringUtils.isNotEmpty(event.oldEmail()) && !event.oldEmail().equals(event.newEmail())) {
keys.add(new UserCacheKey(event.tenantId(), event.oldEmail()));
}
cache.evict(keys);
}
@Override
public User findUserByEmail(TenantId tenantId, String email) {
@ -113,7 +131,16 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
log.trace("Executing findUserByTenantIdAndEmail [{}][{}]", tenantId, email);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateString(email, e -> "Incorrect email " + e);
return userDao.findByTenantIdAndEmail(tenantId, email);
return cache.getAndPutInTransaction(new UserCacheKey(tenantId, email),
() -> userDao.findByTenantIdAndEmail(tenantId, email), true);
}
@Override
public ListenableFuture<User> findUserByTenantIdAndEmailAsync(TenantId tenantId, String email) {
log.trace("Executing findUserByTenantIdAndEmailAsync [{}][{}]", tenantId, email);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateString(email, e -> "Incorrect email " + e);
return executor.submit(() -> findUserByTenantIdAndEmail(tenantId, email));
}
@Override
@ -131,28 +158,38 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
@Override
@Transactional
public User saveUser(TenantId tenantId, User user) {
log.trace("Executing saveUser [{}]", user);
User oldUser = userValidator.validate(user, User::getTenantId);
if (!userLoginCaseSensitive) {
user.setEmail(user.getEmail().toLowerCase());
}
User savedUser = userDao.save(user.getTenantId(), user);
if (user.getId() == null) {
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentialsDao.save(user.getTenantId(), userCredentials);
var evictEvent = new UserCacheEvictEvent(user.getTenantId(), user.getEmail(), oldUser != null ? oldUser.getEmail() : null);
User savedUser;
try {
savedUser = userDao.saveAndFlush(user.getTenantId(), user);
publishEvictEvent(evictEvent);
if (user.getId() == null) {
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentialsDao.save(user.getTenantId(), userCredentials);
}
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId)
.entity(savedUser)
.oldEntity(oldUser)
.entityId(savedUser.getId())
.created(user.getId() == null).build());
} catch (Exception t) {
handleEvictEvent(evictEvent);
checkConstraintViolation(t, "tb_user_email_key", "User with email '" + user.getEmail() + "' already present in database!");
throw t;
}
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId)
.entity(savedUser)
.oldEntity(oldUser)
.entityId(savedUser.getId())
.created(user.getId() == null).build());
return savedUser;
}
@ -262,8 +299,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
validateId(userId, id -> INCORRECT_USER_ID + id);
userCredentialsDao.removeByUserId(tenantId, userId);
userAuthSettingsDao.removeByUserId(userId);
publishEvictEvent(new UserCacheEvictEvent(user.getTenantId(), user.getEmail(), null));
userSettingsDao.removeByUserId(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.USER);

2
dao/src/main/resources/sql/schema-entities.sql

@ -144,6 +144,8 @@ CREATE TABLE IF NOT EXISTS customer (
title varchar(255),
zip varchar(255),
external_id uuid,
is_public boolean,
CONSTRAINT customer_title_unq_key UNIQUE (tenant_id, title),
CONSTRAINT customer_external_id_unq_key UNIQUE (tenant_id, external_id)
);

86
dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java

@ -26,6 +26,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
@ -37,9 +38,14 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@DaoSqlTest
public class CustomerServiceTest extends AbstractServiceTest {
@ -54,7 +60,7 @@ public class CustomerServiceTest extends AbstractServiceTest {
@Before
public void before() {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
}
}
@After
public void after() {
@ -181,7 +187,7 @@ public class CustomerServiceTest extends AbstractServiceTest {
}
@Test
public void testFindCustomersByTenantIdAndTitle() throws Exception {
public void testFindCustomersByTenantIdAndTitleAsTextSearch() throws Exception {
String title1 = "Customer title 1";
List<ListenableFuture<Customer>> futures = new ArrayList<>(143);
for (int i = 0; i < 143; i++) {
@ -261,4 +267,80 @@ public class CustomerServiceTest extends AbstractServiceTest {
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
@Test
public void testFindCustomerByTitle() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle("My customer");
Customer savedCustomer = customerService.saveCustomer(customer);
Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
Optional<Customer> foundCustomerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, savedCustomer.getTitle());
Assert.assertTrue(foundCustomerOpt.isPresent());
Assert.assertEquals(foundCustomerOpt.get().getTitle(), savedCustomer.getTitle());
customerService.deleteCustomer(tenantId, savedCustomer.getId());
}
@Test
public void testSaveCustomerWithExistingTitle() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle("My customer");
Customer savedCustomer = customerService.saveCustomer(customer);
Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
assertThatThrownBy(() -> customerService.saveCustomer(customer))
.isInstanceOf(DataValidationException.class)
.hasMessage("Customer with such title already exists!");
customerService.deleteCustomer(tenantId, savedCustomer.getId());
}
@Test
public void testFindOrCreatePublicCustomer_Concurrency() throws Exception {
CountDownLatch allThreadsReadyLatch = new CountDownLatch(2);
final Customer[] customers = new Customer[2];
ExecutorService executor = null;
try {
executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
final int threadIndex = i;
executor.submit(() -> {
allThreadsReadyLatch.countDown();
try {
allThreadsReadyLatch.await();
customers[threadIndex] = customerService.findOrCreatePublicCustomer(tenantId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
Awaitility.await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated);
Customer firstCustomer = customers[0];
Customer secondCustomer = customers[1];
assertThat(firstCustomer).isNotNull();
assertThat(secondCustomer).isNotNull();
assertThat(firstCustomer).isEqualTo(secondCustomer);
} finally {
if (executor != null) {
executor.shutdownNow();
}
}
}
}

43
dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java

@ -15,10 +15,10 @@
*/
package org.thingsboard.server.dao.service;
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
@ -72,7 +72,7 @@ public class UserServiceTest extends AbstractServiceTest {
customerUser.setEmail("customer@thingsboard.org");
customerUser = userService.saveUser(tenantId, customerUser);
userSettings = createUserSettings(customerUser.getId());
UserSettings userSettings = createUserSettings(customerUser.getId());
}
@Test
@ -90,6 +90,21 @@ public class UserServiceTest extends AbstractServiceTest {
Assert.assertNull(user);
}
@Test
public void testFindUserByTenantIdAndEmail() {
User user = userService.findUserByTenantIdAndEmail(SYSTEM_TENANT_ID, "sysadmin@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.SYS_ADMIN, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "tenant@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.TENANT_ADMIN, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "customer@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.CUSTOMER_USER, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "fake@thingsboard.org");
Assert.assertNull(user);
}
@Test
public void testFindUserById() {
User user = userService.findUserByEmail(SYSTEM_TENANT_ID, "sysadmin@thingsboard.org");
@ -142,36 +157,36 @@ public class UserServiceTest extends AbstractServiceTest {
public void testSaveUserWithSameEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail("sysadmin@thingsboard.org");
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("User with email 'sysadmin@thingsboard.org' already present in database!");
}
@Test
public void testSaveUserWithInvalidEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail("tenant_thingsboard.org");
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("Invalid email address format 'tenant_thingsboard.org'!");
}
@Test
public void testSaveUserWithEmptyEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail(null);
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("User email should be specified!");
}
@Test
public void testSaveUserWithoutTenant() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setTenantId(null);
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("Tenant administrator should be assigned to tenant!");
}
@Test

29
dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java

@ -19,12 +19,14 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.customer.CustomerServiceImpl;
import java.util.Optional;
import java.util.UUID;
@ -67,11 +69,27 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest {
createCustomer(tenantId, i);
}
Optional<Customer> customerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId, "CUSTOMER_5");
Optional<Customer> customerOpt = customerDao.findCustomerByTenantIdAndTitle(tenantId, "CUSTOMER_5");
assertTrue(customerOpt.isPresent());
assertEquals("CUSTOMER_5", customerOpt.get().getTitle());
}
@Test
public void testFindPublicCustomerByTenantId() {
UUID tenantId = Uuids.timeBased();
Optional<Customer> customerOpt = customerDao.findPublicCustomerByTenantId(tenantId);
assertTrue(customerOpt.isEmpty());
String publicCustomerTitle = StringUtils.randomAlphanumeric(10);
createPublicCustomer(tenantId, publicCustomerTitle);
customerOpt = customerDao.findPublicCustomerByTenantId(tenantId);
assertTrue(customerOpt.isPresent());
Customer customer = customerOpt.get();
assertTrue(customer.isPublic());
assertEquals(publicCustomerTitle, customer.getTitle());
}
private void createCustomer(UUID tenantId, int index) {
Customer customer = new Customer();
customer.setId(new CustomerId(Uuids.timeBased()));
@ -79,4 +97,13 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest {
customer.setTitle("CUSTOMER_" + index);
customerDao.save(TenantId.fromUUID(tenantId), customer);
}
private void createPublicCustomer(UUID tenantId, String publicCustomerTitle) {
Customer customer = new Customer();
customer.setId(new CustomerId(Uuids.timeBased()));
customer.setTenantId(TenantId.fromUUID(tenantId));
customer.setTitle(publicCustomerTitle);
customer.setAdditionalInfo(CustomerServiceImpl.PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON);
customerDao.save(TenantId.fromUUID(tenantId), customer);
}
}

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

@ -26,6 +26,12 @@ cache.specs.sessions.maxSize=100000
cache.specs.assets.timeToLiveInMinutes=1440
cache.specs.assets.maxSize=100000
cache.specs.customers.timeToLiveInMinutes=1440
cache.specs.customers.maxSize=10000
cache.specs.users.timeToLiveInMinutes=1440
cache.specs.users.maxSize=10000
cache.specs.entityViews.timeToLiveInMinutes=1440
cache.specs.entityViews.maxSize=100000

6
monitoring/src/main/resources/lwm2m/device_profile.json

@ -12,14 +12,14 @@
"transportConfiguration": {
"observeAttr": {
"observe": [
"/3_1.0/0/0"
"/3_1.1/0/0"
],
"attribute": [],
"telemetry": [
"/3_1.0/0/0"
"/3_1.1/0/0"
],
"keyName": {
"/3_1.0/0/0": "testData"
"/3_1.1/0/0": "testData"
},
"attributeLwm2m": {}
},

4
monitoring/src/main/resources/lwm2m/models/test-model.xml

@ -23,9 +23,9 @@
<Description1>
<![CDATA[]]></Description1>
<ObjectID>3</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:3:1.0</ObjectURN>
<ObjectURN>urn:oma:lwm2m:oma:3:1.1</ObjectURN>
<LWM2MVersion>1.1</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<ObjectVersion>1.1</ObjectVersion>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Resources>

2
monitoring/src/main/resources/lwm2m/resource.json

@ -2,5 +2,5 @@
"title": "",
"resourceType": "LWM2M_MODEL",
"fileName": "test-model.xml",
"data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMDwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4wPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K"
"data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMTwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4xPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K"
}

3
msa/tb-node/docker/Dockerfile

@ -18,6 +18,9 @@ FROM thingsboard/openjdk17:bookworm-slim
COPY start-tb-node.sh ${pkg.name}.deb /tmp/
# Required for SWAGGER UI when reverse proxy is used
ENV HTTP_FORWARD_HEADERS_STRATEGY=framework
RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-tb-node.sh /usr/bin && \
(yes | dpkg -i /tmp/${pkg.name}.deb) && \

10
netty-mqtt/pom.xml

@ -82,21 +82,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.takari.junit</groupId>
<artifactId>takari-cpsuite</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

18
netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java

@ -23,10 +23,11 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
@ -39,6 +40,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ResourceLock("port8885") // test MQTT server port
@Slf4j
public class MqttIntegrationTest {
@ -53,7 +55,7 @@ public class MqttIntegrationTest {
AbstractListeningExecutor handlerExecutor;
@Before
@BeforeEach
public void init() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
@ -69,7 +71,7 @@ public class MqttIntegrationTest {
this.mqttServer.init();
}
@After
@AfterEach
public void destroy() throws InterruptedException {
if (this.mqttClient != null) {
this.mqttClient.disconnect();
@ -100,7 +102,7 @@ public class MqttIntegrationTest {
log.warn("Waiting for messages acknowledgments...");
boolean awaitResult = latch.await(10, TimeUnit.SECONDS);
Assert.assertTrue(awaitResult);
Assertions.assertTrue(awaitResult);
log.warn("Messages are delivered successfully...");
//when
@ -111,7 +113,7 @@ public class MqttIntegrationTest {
List<MqttMessageType> allReceivedEvents = this.mqttServer.getEventsFromClient();
long disconnectCount = allReceivedEvents.stream().filter(type -> type == MqttMessageType.DISCONNECT).count();
Assert.assertEquals(1, disconnectCount);
Assertions.assertEquals(1, disconnectCount);
}
private Future<Void> publishMsg() {

2
netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/server/MqttServer.java

@ -46,7 +46,7 @@ public class MqttServer {
private EventLoopGroup workerGroup;
public void init() throws Exception {
log.info("Starting MQTT server...");
log.info("Starting MQTT server on port {}...", mqttPort);
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();

3
netty-mqtt/src/test/resources/junit-platform.properties

@ -0,0 +1,3 @@
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent

14
netty-mqtt/src/test/resources/logback-test.xml

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
</root>
</configuration>

2
pom.xml

@ -1548,7 +1548,7 @@
<version>${json-schema-validator.version}</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<groupId>com.sun.mail</groupId>
<artifactId>mailapi</artifactId>
</exclusion>
</exclusions>

144
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java

@ -15,13 +15,11 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.Data;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -29,31 +27,32 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.EnumSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractCustomerActionNode<C extends TbAbstractCustomerActionNodeConfiguration> implements TbNode {
protected C config;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.ASSET, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
private LoadingCache<CustomerKey, Optional<CustomerId>> customerIdCache;
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadCustomerNodeActionConfig(configuration);
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
if (this.config.getCustomerCacheExpiration() > 0) {
cacheBuilder.expireAfterWrite(this.config.getCustomerCacheExpiration(), TimeUnit.SECONDS);
}
customerIdCache = cacheBuilder
.build(new CustomerCacheLoader(ctx, createCustomerIfNotExists()));
}
protected abstract boolean createCustomerIfNotExists();
@ -62,77 +61,70 @@ public abstract class TbAbstractCustomerActionNode<C extends TbAbstractCustomerA
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var entityType = msg.getOriginator().getEntityType();
if (!supportedEntityTypes.contains(entityType)) {
throw new RuntimeException(unsupportedOriginatorTypeErrorMessage(entityType));
}
withCallback(processCustomerAction(ctx, msg),
m -> ctx.tellSuccess(msg),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
ListenableFuture<CustomerId> customerIdFeature = getCustomer(ctx, msg);
return Futures.transform(customerIdFeature, customerId -> {
doProcessCustomerAction(ctx, msg, customerId);
return null;
}, ctx.getDbCallbackExecutor()
);
}
protected abstract void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId);
protected ListenableFuture<CustomerId> getCustomer(TbContext ctx, TbMsg msg) {
String customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg);
CustomerKey key = new CustomerKey(customerTitle);
return ctx.getDbCallbackExecutor().executeAsync(() -> {
Optional<CustomerId> customerId = customerIdCache.get(key);
if (!customerId.isPresent()) {
throw new RuntimeException("No customer found with name '" + key.getCustomerTitle() + "'.");
}
return customerId.get();
});
}
@Override
public void destroy() {
if (customerIdCache != null) {
customerIdCache.invalidateAll();
protected abstract ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg);
protected ListenableFuture<CustomerId> getCustomerIdFuture(TbContext ctx, TbMsg msg) {
var tenantId = ctx.getTenantId();
var customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg);
var customerService = ctx.getCustomerService();
var customerByTitleFuture = customerService.findCustomerByTenantIdAndTitleAsync(tenantId, customerTitle);
if (createCustomerIfNotExists()) {
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
try {
var newCustomer = new Customer();
newCustomer.setTitle(customerTitle);
newCustomer.setTenantId(tenantId);
var savedCustomer = customerService.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return savedCustomer.getId();
} catch (DataValidationException e) {
customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle);
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
throw new RuntimeException("Failed to create customer with title '" + customerTitle + "' due to: ", e);
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isEmpty()) {
throw new NoSuchElementException("Customer with title '" + customerTitle + "' doesn't exist!");
}
return customerOpt.get().getId();
}, MoreExecutors.directExecutor());
}
@Data
@AllArgsConstructor
private static class CustomerKey {
private String customerTitle;
private static String unsupportedOriginatorTypeErrorMessage(EntityType originatorType) {
return "Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.";
}
private static class CustomerCacheLoader extends CacheLoader<CustomerKey, Optional<CustomerId>> {
private final TbContext ctx;
private final boolean createIfNotExists;
private CustomerCacheLoader(TbContext ctx, boolean createIfNotExists) {
this.ctx = ctx;
this.createIfNotExists = createIfNotExists;
}
@Override
public Optional<CustomerId> load(CustomerKey key) {
CustomerService service = ctx.getCustomerService();
Optional<Customer> customerOptional =
service.findCustomerByTenantIdAndTitle(ctx.getTenantId(), key.getCustomerTitle());
if (customerOptional.isPresent()) {
return Optional.of(customerOptional.get().getId());
} else if (createIfNotExists) {
Customer newCustomer = new Customer();
newCustomer.setTitle(key.getCustomerTitle());
newCustomer.setTenantId(ctx.getTenantId());
Customer savedCustomer = service.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return Optional.of(savedCustomer.getId());
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) {
boolean hasChanges = false;
switch (fromVersion) {
case 0 -> {
if (oldConfiguration.has("customerCacheExpiration")) {
((ObjectNode) oldConfiguration).remove("customerCacheExpiration");
hasChanges = true;
}
}
return Optional.empty();
}
return new TbPair<>(hasChanges, oldConfiguration);
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNodeConfiguration.java

@ -21,6 +21,5 @@ import lombok.Data;
public abstract class TbAbstractCustomerActionNodeConfiguration {
private String customerNamePattern;
private long customerCacheExpiration;
}

422
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java

@ -15,269 +15,275 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.FAILURE;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS;
import java.util.EnumSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@Slf4j
public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationActionNodeConfiguration> implements TbNode {
protected C config;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
private LoadingCache<EntityKey, EntityContainer> entityIdCache;
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadEntityNodeActionConfig(configuration);
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
if (this.config.getEntityCacheExpiration() > 0) {
cacheBuilder.expireAfterWrite(this.config.getEntityCacheExpiration(), TimeUnit.SECONDS);
}
entityIdCache = cacheBuilder.build(new EntityCacheLoader(ctx, createEntityIfNotExists()));
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
String relationType = processPattern(msg, config.getRelationType());
withCallback(processEntityRelationAction(ctx, msg, relationType),
filterResult -> ctx.tellNext(filterResult.getMsg(), filterResult.isResult() ? SUCCESS : FAILURE), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
@Override
public void destroy() {
if (entityIdCache != null) {
entityIdCache.invalidateAll();
}
}
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
}
protected abstract boolean createEntityIfNotExists();
protected abstract ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType);
protected abstract C loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException;
protected ListenableFuture<EntityContainer> getEntity(TbContext ctx, TbMsg msg) {
String entityName = processPattern(msg, this.config.getEntityNamePattern());
String type;
if (this.config.getEntityTypePattern() != null) {
type = processPattern(msg, this.config.getEntityTypePattern());
} else {
type = null;
}
EntityType entityType = EntityType.valueOf(this.config.getEntityType());
EntityKey key = new EntityKey(entityName, type, entityType);
return ctx.getDbCallbackExecutor().executeAsync(() -> {
EntityContainer entityContainer = entityIdCache.get(key);
if (entityContainer.getEntityId() == null) {
throw new RuntimeException("No entity found with type '" + key.getEntityType() + "' and name '" + key.getEntityName() + "'.");
}
return entityContainer;
});
}
protected SearchDirectionIds processSingleSearchDirection(TbMsg msg, EntityContainer entityContainer) {
SearchDirectionIds searchDirectionIds = new SearchDirectionIds();
if (EntitySearchDirection.FROM.name().equals(this.config.getDirection())) {
searchDirectionIds.setFromId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setToId(msg.getOriginator());
searchDirectionIds.setOriginatorDirectionFrom(false);
} else {
searchDirectionIds.setToId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setFromId(msg.getOriginator());
searchDirectionIds.setOriginatorDirectionFrom(true);
}
return searchDirectionIds;
}
protected ListenableFuture<List<EntityRelation>> processListSearchDirection(TbContext ctx, TbMsg msg) {
if (EntitySearchDirection.FROM.name().equals(this.config.getDirection())) {
return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), processPattern(msg, this.config.getRelationType()), RelationTypeGroup.COMMON);
} else {
return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), processPattern(msg, this.config.getRelationType()), RelationTypeGroup.COMMON);
}
}
protected String processPattern(TbMsg msg, String pattern) {
return TbNodeUtils.processPattern(pattern, msg);
}
@Data
@AllArgsConstructor
private static class EntityKey {
private String entityName;
private String type;
private EntityType entityType;
}
@Data
protected static class SearchDirectionIds {
private EntityId fromId;
private EntityId toId;
private boolean originatorDirectionFrom;
}
private static class EntityCacheLoader extends CacheLoader<EntityKey, EntityContainer> {
private final TbContext ctx;
private final boolean createIfNotExists;
private EntityCacheLoader(TbContext ctx, boolean createIfNotExists) {
this.ctx = ctx;
this.createIfNotExists = createIfNotExists;
protected ListenableFuture<EntityId> getTargetEntityId(TbContext ctx, TbMsg msg) {
var entityType = config.getEntityType();
var tenantId = ctx.getTenantId();
if (EntityType.TENANT.equals(entityType)) {
return ctx.getDbCallbackExecutor().executeAsync(() -> tenantId);
}
@Override
public EntityContainer load(EntityKey key) {
return loadEntity(key);
}
private EntityContainer loadEntity(EntityKey entitykey) {
EntityType type = entitykey.getEntityType();
EntityContainer targetEntity = new EntityContainer();
targetEntity.setEntityType(type);
switch (type) {
case DEVICE:
DeviceService deviceService = ctx.getDeviceService();
Device device = deviceService.findDeviceByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (device != null) {
targetEntity.setEntityId(device.getId());
} else if (createIfNotExists) {
Device newDevice = new Device();
newDevice.setName(entitykey.getEntityName());
newDevice.setType(entitykey.getType());
newDevice.setTenantId(ctx.getTenantId());
Device savedDevice = deviceService.saveDevice(newDevice);
ctx.enqueue(ctx.deviceCreatedMsg(savedDevice, ctx.getSelfId()),
() -> log.trace("Pushed Device Created message: {}", savedDevice),
throwable -> log.warn("Failed to push Device Created message: {}", savedDevice, throwable));
targetEntity.setEntityId(savedDevice.getId());
var targetEntityName = processPattern(msg, config.getEntityNamePattern());
boolean createEntityIfNotExists = createEntityIfNotExists();
switch (entityType) {
case DEVICE -> {
var deviceService = ctx.getDeviceService();
var deviceByNameFuture = deviceService.findDeviceByTenantIdAndNameAsync(tenantId, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(deviceByNameFuture, device -> {
if (device != null) {
return device.getId();
}
try {
var deviceProfileName = processPattern(msg, config.getEntityTypePattern());
var newDevice = new Device();
newDevice.setName(targetEntityName);
newDevice.setType(deviceProfileName);
newDevice.setTenantId(tenantId);
var savedDevice = deviceService.saveDevice(newDevice);
ctx.getClusterService().onDeviceUpdated(savedDevice, null);
ctx.enqueue(ctx.deviceCreatedMsg(savedDevice, ctx.getSelfId()),
() -> log.trace("Pushed Device Created message: {}", savedDevice),
throwable -> log.warn("Failed to push Device Created message: {}", savedDevice, throwable));
return savedDevice.getId();
} catch (DataValidationException e) {
device = deviceService.findDeviceByTenantIdAndName(tenantId, targetEntityName);
if (device != null) {
return device.getId();
}
throw new RuntimeException("Failed to create device with name '" + targetEntityName + "' due to: ", e);
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(deviceByNameFuture, device -> {
if (device == null) {
throw new NoSuchElementException("Device with name '" + targetEntityName + "' doesn't exist!");
}
break;
case ASSET:
AssetService assetService = ctx.getAssetService();
Asset asset = assetService.findAssetByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (asset != null) {
targetEntity.setEntityId(asset.getId());
} else if (createIfNotExists) {
Asset newAsset = new Asset();
newAsset.setName(entitykey.getEntityName());
newAsset.setType(entitykey.getType());
newAsset.setTenantId(ctx.getTenantId());
Asset savedAsset = assetService.saveAsset(newAsset);
ctx.enqueue(ctx.assetCreatedMsg(savedAsset, ctx.getSelfId()),
() -> log.trace("Pushed Asset Created message: {}", savedAsset),
throwable -> log.warn("Failed to push Asset Created message: {}", savedAsset, throwable));
targetEntity.setEntityId(savedAsset.getId());
return device.getId();
}, MoreExecutors.directExecutor());
}
case ASSET -> {
var assetService = ctx.getAssetService();
var assetByNameFuture = assetService.findAssetByTenantIdAndNameAsync(tenantId, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(assetByNameFuture, asset -> {
if (asset != null) {
return asset.getId();
}
try {
var assetProfileName = processPattern(msg, config.getEntityTypePattern());
var newAsset = new Asset();
newAsset.setName(targetEntityName);
newAsset.setType(assetProfileName);
newAsset.setTenantId(tenantId);
var savedAsset = assetService.saveAsset(newAsset);
ctx.enqueue(ctx.assetCreatedMsg(savedAsset, ctx.getSelfId()),
() -> log.trace("Pushed Asset Created message: {}", savedAsset),
throwable -> log.warn("Failed to push Asset Created message: {}", savedAsset, throwable));
return savedAsset.getId();
} catch (DataValidationException e) {
asset = assetService.findAssetByTenantIdAndName(tenantId, targetEntityName);
if (asset != null) {
return asset.getId();
}
throw new RuntimeException("Failed to create asset with name '" + targetEntityName + "' due to: ", e);
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(assetByNameFuture, asset -> {
if (asset == null) {
throw new NoSuchElementException("Asset with name '" + targetEntityName + "' doesn't exist!");
}
break;
case CUSTOMER:
CustomerService customerService = ctx.getCustomerService();
Optional<Customer> customerOptional = customerService.findCustomerByTenantIdAndTitle(ctx.getTenantId(), entitykey.getEntityName());
if (customerOptional.isPresent()) {
targetEntity.setEntityId(customerOptional.get().getId());
} else if (createIfNotExists) {
Customer newCustomer = new Customer();
newCustomer.setTitle(entitykey.getEntityName());
newCustomer.setTenantId(ctx.getTenantId());
Customer savedCustomer = customerService.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
targetEntity.setEntityId(savedCustomer.getId());
return asset.getId();
}, MoreExecutors.directExecutor());
}
case CUSTOMER -> {
var customerService = ctx.getCustomerService();
var customerByTitleOptFuture = customerService.findCustomerByTenantIdAndTitleAsync(tenantId, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(customerByTitleOptFuture, customerOpt -> {
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
try {
var newCustomer = new Customer();
newCustomer.setTitle(targetEntityName);
newCustomer.setTenantId(tenantId);
var savedCustomer = customerService.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return savedCustomer.getId();
} catch (DataValidationException e) {
customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, targetEntityName);
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
throw new RuntimeException("Failed to create customer with title '" + targetEntityName + "' due to: ", e);
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(customerByTitleOptFuture, customerOpt -> {
if (customerOpt.isEmpty()) {
throw new NoSuchElementException("Customer with title '" + targetEntityName + "' doesn't exist!");
}
break;
case TENANT:
targetEntity.setEntityId(ctx.getTenantId());
break;
case ENTITY_VIEW:
EntityViewService entityViewService = ctx.getEntityViewService();
EntityView entityView = entityViewService.findEntityViewByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
return customerOpt.get().getId();
}, MoreExecutors.directExecutor());
}
case ENTITY_VIEW -> {
var entityViewFuture = ctx.getEntityViewService().findEntityViewByTenantIdAndNameAsync(tenantId, targetEntityName);
return Futures.transform(entityViewFuture, entityView -> {
if (entityView != null) {
targetEntity.setEntityId(entityView.getId());
return entityView.getId();
}
break;
case EDGE:
EdgeService edgeService = ctx.getEdgeService();
Edge edge = edgeService.findEdgeByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
throw new NoSuchElementException("Entity View with name '" + targetEntityName + "' doesn't exist!");
}, MoreExecutors.directExecutor());
}
case EDGE -> {
var edgeFuture = ctx.getEdgeService().findEdgeByTenantIdAndNameAsync(tenantId, targetEntityName);
return Futures.transform(edgeFuture, edge -> {
if (edge != null) {
targetEntity.setEntityId(edge.getId());
return edge.getId();
}
break;
case DASHBOARD:
DashboardService dashboardService = ctx.getDashboardService();
DashboardInfo dashboardInfo = dashboardService.findFirstDashboardInfoByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
throw new NoSuchElementException("Edge with name '" + targetEntityName + "' doesn't exist!");
}, MoreExecutors.directExecutor());
}
case DASHBOARD -> {
var dashboardInfoFuture = ctx.getDashboardService().findFirstDashboardInfoByTenantIdAndNameAsync(tenantId, targetEntityName);
return Futures.transform(dashboardInfoFuture, dashboardInfo -> {
if (dashboardInfo != null) {
targetEntity.setEntityId(dashboardInfo.getId());
return dashboardInfo.getId();
}
break;
case USER:
UserService userService = ctx.getUserService();
User user = userService.findUserByTenantIdAndEmail(ctx.getTenantId(), entitykey.getEntityName());
throw new NoSuchElementException("Dashboard with title '" + targetEntityName + "' doesn't exist!");
}, MoreExecutors.directExecutor());
}
case USER -> {
var userFuture = ctx.getUserService().findUserByTenantIdAndEmailAsync(tenantId, targetEntityName);
return Futures.transform(userFuture, user -> {
if (user != null) {
targetEntity.setEntityId(user.getId());
return user.getId();
}
break;
default:
return targetEntity;
throw new NoSuchElementException("User with email '" + targetEntityName + "' doesn't exist!");
}, MoreExecutors.directExecutor());
}
return targetEntity;
default -> throw new IllegalArgumentException(unsupportedEntityTypeErrorMessage(entityType));
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
protected static class RelationContainer {
protected ListenableFuture<Boolean> deleteRelationsByTypeAndDirection(TbContext ctx, TbMsg msg, Executor executor) {
var relationType = processPattern(msg, config.getRelationType());
return deleteRelationsByTypeAndDirection(ctx, msg, relationType, executor);
}
protected ListenableFuture<Boolean> deleteRelationsByTypeAndDirection(TbContext ctx, TbMsg msg, String relationType, Executor executor) {
var tenantId = ctx.getTenantId();
var originator = msg.getOriginator();
var relationService = ctx.getRelationService();
var originatorRelationsFuture = EntitySearchDirection.FROM.equals(config.getDirection()) ?
relationService.findByFromAndTypeAsync(tenantId, originator, relationType, RelationTypeGroup.COMMON) :
relationService.findByToAndTypeAsync(tenantId, originator, relationType, RelationTypeGroup.COMMON);
return Futures.transformAsync(originatorRelationsFuture, originatorRelations -> {
if (originatorRelations.isEmpty()) {
return Futures.immediateFuture(true);
}
var deleteRelationFutures = originatorRelations.stream()
.map(entityRelation -> relationService.deleteRelationAsync(tenantId, entityRelation))
.collect(Collectors.toList());
return Futures.transform(Futures.allAsList(deleteRelationFutures), deleteResults ->
deleteResults.stream().allMatch(Boolean::booleanValue), executor);
}, executor);
}
private TbMsg msg;
private boolean result;
protected void checkIfConfigEntityTypeIsSupported(EntityType entityType) throws TbNodeException {
if (!supportedEntityTypes.contains(entityType)) {
throw new TbNodeException(unsupportedEntityTypeErrorMessage(entityType), true);
}
}
private static String unsupportedEntityTypeErrorMessage(EntityType entityType) {
return "Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.";
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
var config = (ObjectNode) oldConfiguration;
switch (fromVersion) {
case 0 -> {
if (!config.has("entityCacheExpiration")) {
break;
}
config.remove("entityCacheExpiration");
var directionPropertyName = "direction";
if (!config.has(directionPropertyName)) {
throw new TbNodeException("property to update: '" + directionPropertyName + "' doesn't exists in configuration!");
}
String direction = config.get(directionPropertyName).asText();
if (EntitySearchDirection.TO.name().equals(direction)) {
config.put(directionPropertyName, EntitySearchDirection.FROM.name());
hasChanges = true;
break;
}
if (EntitySearchDirection.FROM.name().equals(direction)) {
config.put(directionPropertyName, EntitySearchDirection.TO.name());
hasChanges = true;
break;
}
throw new TbNodeException("property to update: '" + directionPropertyName + "' has invalid value!");
}
}
return new TbPair<>(hasChanges, config);
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNodeConfiguration.java

@ -16,17 +16,17 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
public abstract class TbAbstractRelationActionNodeConfiguration {
private String direction;
private EntitySearchDirection direction;
private String relationType;
private String entityType;
private EntityType entityType;
private String entityNamePattern;
private String entityTypePattern;
private long entityCacheExpiration;
}

79
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java

@ -15,15 +15,16 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -36,12 +37,13 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.ACTION,
name = "assign to customer",
configClazz = TbAssignToCustomerNodeConfiguration.class,
nodeDescription = "Assign Message Originator Entity to Customer",
nodeDetails = "Finds target Customer by customer name pattern and then assign Originator Entity to this customer. " +
"Will create new Customer if it doesn't exists and 'Create new Customer if not exists' is set to true.",
nodeDescription = "Assign message originator entity to customer",
nodeDetails = "Finds target customer by title and assign message originator entity to this customer. " +
"Rule node will create a new customer if it doesn't exist, and 'Create new customer if it doesn't exist' enabled.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeAssignToCustomerConfig",
icon = "add_circle"
icon = "add_circle",
version = 1
)
public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssignToCustomerNodeConfiguration> {
@ -56,53 +58,24 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssig
}
@Override
protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
processAssign(ctx, msg, customerId);
}
private void processAssign(TbContext ctx, TbMsg msg, CustomerId customerId) {
EntityType originatorType = msg.getOriginator().getEntityType();
switch (originatorType) {
case DEVICE:
processAssignDevice(ctx, msg, customerId);
break;
case ASSET:
processAssignAsset(ctx, msg, customerId);
break;
case ENTITY_VIEW:
processAssignEntityView(ctx, msg, customerId);
break;
case EDGE:
processAssignEdge(ctx, msg, customerId);
break;
case DASHBOARD:
processAssignDashboard(ctx, msg, customerId);
break;
default:
ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + originatorType +
"'! Only 'DEVICE', 'ASSET', 'ENTITY_VIEW' or 'DASHBOARD' types are allowed."));
break;
}
}
private void processAssignAsset(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getAssetService().assignAssetToCustomer(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()), customerId);
}
private void processAssignDevice(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDeviceService().assignDeviceToCustomer(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()), customerId);
}
private void processAssignEntityView(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getEntityViewService().assignEntityViewToCustomer(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()), customerId);
}
private void processAssignEdge(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getEdgeService().assignEdgeToCustomer(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId()), customerId);
}
private void processAssignDashboard(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDashboardService().assignDashboardToCustomer(ctx.getTenantId(), new DashboardId(msg.getOriginator().getId()), customerId);
protected ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
var customerIdFuture = getCustomerIdFuture(ctx, msg);
return Futures.transform(customerIdFuture, customerId -> {
var originator = msg.getOriginator();
switch (originator.getEntityType()) {
case ASSET ->
ctx.getAssetService().assignAssetToCustomer(ctx.getTenantId(), new AssetId(originator.getId()), customerId);
case DEVICE ->
ctx.getDeviceService().assignDeviceToCustomer(ctx.getTenantId(), new DeviceId(originator.getId()), customerId);
case ENTITY_VIEW ->
ctx.getEntityViewService().assignEntityViewToCustomer(ctx.getTenantId(), new EntityViewId(originator.getId()), customerId);
case EDGE ->
ctx.getEdgeService().assignEdgeToCustomer(ctx.getTenantId(), new EdgeId(originator.getId()), customerId);
case DASHBOARD ->
ctx.getDashboardService().assignDashboardToCustomer(ctx.getTenantId(), new DashboardId(originator.getId()), customerId);
}
return null;
}, MoreExecutors.directExecutor());
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeConfiguration.java

@ -16,19 +16,20 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbAssignToCustomerNodeConfiguration extends TbAbstractCustomerActionNodeConfiguration implements NodeConfiguration<TbAssignToCustomerNodeConfiguration> {
private boolean createCustomerIfNotExists;
@Override
public TbAssignToCustomerNodeConfiguration defaultConfiguration() {
TbAssignToCustomerNodeConfiguration configuration = new TbAssignToCustomerNodeConfiguration();
var configuration = new TbAssignToCustomerNodeConfiguration();
configuration.setCustomerNamePattern("");
configuration.setCreateCustomerIfNotExists(false);
configuration.setCustomerCacheExpiration(300);
return configuration;
}
}

254
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java

@ -17,52 +17,62 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "create relation",
configClazz = TbCreateRelationNodeConfiguration.class,
nodeDescription = "Finds target Entity by entity name pattern and (entity type pattern for Asset, Device) and then create a relation to Originator Entity by type and direction." +
" If Selected entity type: Asset, Device or Customer will create new Entity if it doesn't exist and selected checkbox 'Create new entity if not exists'.<br>" +
" In case that relation from the message originator to the selected entity not exist and If selected checkbox 'Remove current relations'," +
" before creating the new relation all existed relations to message originator by type and direction will be removed.<br>" +
" If relation from the message originator to the selected entity created and If selected checkbox 'Change originator to related entity'," +
" outbound message will be processed as a message from this entity.",
nodeDetails = "If the relation already exists or successfully created - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
nodeDescription = "Finds target entity specified in the configuration and creates a relation with the " +
"incoming message originator based on the configured direction and type.",
nodeDetails = "Useful when you need to create relations between entities dynamically depending on " +
"incoming message payload, message originator type, name, etc.<br><br>" +
"Target entity configuration: " +
"<ul><li><strong>Device</strong> - use a device with the specified name as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable device creation if it doesn't exist. " +
"In advanced mode, device profile name should be specified.</li>" +
"<li><strong>Asset</strong> - use an asset with the specified name as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable device creation if it doesn't exist. " +
"In advanced mode, asset profile name should be specified.</li>" +
"<li><strong>Entity View</strong> - use entity view with the specified name as the target entity to create a relation with.</li>" +
"<li><strong>Tenant</strong> - use current tenant as target entity to create a relation with.</li>" +
"<li><strong>Customer</strong> - use customer with the specified title as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable customer creation if it doesn't exist.</li>" +
"<li><strong>Dashboard</strong> - use a dashboard with the specified title as the target entity to create a relation with.</li>" +
"<li><strong>User</strong> - use a user with the specified email as the target entity to create a relation with.</li>" +
"<li><strong>Edge</strong> - use an edge with the specified name as the target entity to create a relation with.</li></ul>" +
"Advanced settings: " +
"<ul><li><strong>Remove current relations</strong> - removes current relations with originator of the incoming message based on direction and type. " +
"Useful in GPS tracking use cases where relation acts as a temporary indicator of a tracker presence in specific geofence.</li>" +
"<li><strong>Change originator to target entity</strong> - useful when you need to process submitted message as a message from target entity.</li></ul>" +
"Output connections: <code>Success</code> - if the relation already exists or successfully created, otherwise <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCreateRelationConfig",
icon = "add_circle"
icon = "add_circle",
version = 1
)
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {
@Override
protected TbCreateRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
var createRelationNodeConfiguration = TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
checkIfConfigEntityTypeIsSupported(createRelationNodeConfiguration.getEntityType());
return createRelationNodeConfiguration;
}
@Override
@ -71,167 +81,59 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity, String relationType) {
ListenableFuture<Boolean> future = createRelationIfAbsent(ctx, msg, entity, relationType);
return Futures.transform(future, result -> {
if (result && config.isChangeOriginatorToRelatedEntity()) {
TbMsg tbMsg = ctx.transformMsgOriginator(msg, entity.getEntityId());
return new RelationContainer(tbMsg, result);
public void onMsg(TbContext ctx, TbMsg msg) {
var targetEntityIdFuture = getTargetEntityId(ctx, msg);
var createRelationResultFuture = Futures.transformAsync(targetEntityIdFuture, targetEntityId -> {
var originator = msg.getOriginator();
var relationType = processPattern(msg, config.getRelationType());
if (config.isRemoveCurrentRelations()) {
var removalOfCurrentRelationsFuture = deleteRelationsByTypeAndDirection(ctx, msg, relationType, MoreExecutors.directExecutor());
return Futures.transformAsync(removalOfCurrentRelationsFuture, __ ->
checkRelationAndCreateIfAbsent(ctx, originator, targetEntityId, relationType), MoreExecutors.directExecutor());
}
return new RelationContainer(msg, result);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> createRelationIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
return Futures.transformAsync(deleteCurrentRelationsIfNeeded(ctx, msg, sdId, relationType), v ->
checkRelationAndCreateIfAbsent(ctx, entityContainer, relationType, sdId),
ctx.getDbCallbackExecutor());
}
private ListenableFuture<Void> deleteCurrentRelationsIfNeeded(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) {
if (config.isRemoveCurrentRelations()) {
return deleteOriginatorRelations(ctx, findOriginatorRelations(ctx, msg, sdId, relationType));
return checkRelationAndCreateIfAbsent(ctx, originator, targetEntityId, relationType);
}, MoreExecutors.directExecutor());
if (!config.isChangeOriginatorToRelatedEntity()) {
withCallback(createRelationResultFuture,
relationCreated -> {
if (relationCreated) {
ctx.tellSuccess(msg);
return;
}
ctx.tellFailure(msg, new RuntimeException("Failed to create originator relation with target entity!"));
},
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
return;
}
return Futures.immediateFuture(null);
}
private ListenableFuture<List<EntityRelation>> findOriginatorRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) {
if (sdId.isOriginatorDirectionFrom()) {
return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
} else {
return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
}
}
private ListenableFuture<Void> deleteOriginatorRelations(TbContext ctx, ListenableFuture<List<EntityRelation>> originatorRelationsFuture) {
return Futures.transformAsync(originatorRelationsFuture, originatorRelations -> {
List<ListenableFuture<Boolean>> list = new ArrayList<>();
if (!CollectionUtils.isEmpty(originatorRelations)) {
for (EntityRelation relation : originatorRelations) {
list.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation));
}
}
return Futures.transform(Futures.allAsList(list), result -> null, ctx.getDbCallbackExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> checkRelationAndCreateIfAbsent(TbContext ctx, EntityContainer entityContainer, String relationType, SearchDirectionIds sdId) {
return Futures.transformAsync(checkRelation(ctx, sdId, relationType), relationPresent -> {
if (relationPresent) {
return Futures.immediateFuture(true);
}
return processCreateRelation(ctx, entityContainer, sdId, relationType);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> checkRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON);
}
private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
switch (entityContainer.getEntityType()) {
case ASSET:
return processAsset(ctx, entityContainer, sdId, relationType);
case DEVICE:
return processDevice(ctx, entityContainer, sdId, relationType);
case CUSTOMER:
return processCustomer(ctx, entityContainer, sdId, relationType);
case DASHBOARD:
return processDashboard(ctx, entityContainer, sdId, relationType);
case ENTITY_VIEW:
return processView(ctx, entityContainer, sdId, relationType);
case EDGE:
return processEdge(ctx, entityContainer, sdId, relationType);
case TENANT:
return processTenant(ctx, entityContainer, sdId, relationType);
case USER:
return processUser(ctx, entityContainer, sdId, relationType);
}
return Futures.immediateFuture(true);
}
private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
if (entityView != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
withCallback(Futures.allAsList(targetEntityIdFuture, createRelationResultFuture), result -> {
var targetEntityId = (EntityId) result.get(0);
var relationCreated = (Boolean) result.get(1);
if (relationCreated) {
var transformedMsg = ctx.transformMsgOriginator(msg, targetEntityId);
ctx.tellSuccess(transformedMsg);
return;
}
}, ctx.getDbCallbackExecutor());
ctx.tellFailure(msg, new RuntimeException("Failed to create originator relation with target entity!"));
}, t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<Boolean> processEdge(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(entityContainer.getEntityId().getId())), edge -> {
if (edge != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId()));
if (device != null) {
return processSave(ctx, sdId, relationType);
private ListenableFuture<Boolean> checkRelationAndCreateIfAbsent(TbContext ctx, EntityId originator, EntityId targetEntityId, String relationType) {
EntityId fromId;
EntityId toId;
if (EntitySearchDirection.FROM.equals(config.getDirection())) {
fromId = originator;
toId = targetEntityId;
} else {
return Futures.immediateFuture(true);
toId = originator;
fromId = targetEntityId;
}
}
private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
if (asset != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
if (customer != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
if (dashboard != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), TenantId.fromUUID(entityContainer.getEntityId().getId())), tenant -> {
if (tenant != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processUser(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), new UserId(entityContainer.getEntityId().getId())), user -> {
if (user != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSave(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON));
var checkRelationFuture = ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), fromId, toId, relationType, RelationTypeGroup.COMMON);
return Futures.transformAsync(checkRelationFuture, relationExists ->
relationExists ?
Futures.immediateFuture(true) :
ctx.getRelationService().
saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, relationType, RelationTypeGroup.COMMON)),
MoreExecutors.directExecutor());
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeConfiguration.java

@ -16,10 +16,13 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbCreateRelationNodeConfiguration> {
private boolean createEntityIfNotExists;
@ -29,10 +32,9 @@ public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionN
@Override
public TbCreateRelationNodeConfiguration defaultConfiguration() {
TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType("Contains");
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityNamePattern("");
configuration.setEntityCacheExpiration(300);
configuration.setCreateEntityIfNotExists(false);
configuration.setRemoveCurrentRelations(false);
configuration.setChangeOriginatorToRelatedEntity(false);

110
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java

@ -17,20 +17,20 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@ -38,18 +38,36 @@ import java.util.List;
type = ComponentType.ACTION,
name = "delete relation",
configClazz = TbDeleteRelationNodeConfiguration.class,
nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction" +
" if 'Delete single entity' is set to true, otherwise rule node will delete all relations to the originator of the message by type and direction.",
nodeDetails = "If the relation(s) successfully deleted - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
nodeDescription = "Deletes relation with the incoming message originator based on the configured direction and type.",
nodeDetails = "Useful when you need to remove relations between entities dynamically depending on incoming message payload, " +
"message originator type, name, etc.<br><br>" +
"If <strong>Delete relation with specific entity</strong> enabled, target entity to delete relation with should be specified. " +
"Otherwise, rule node will delete all relations with the message originator based on the configured direction and type.<br><br>" +
"Target entity configuration: " +
"<ul><li><strong>Device</strong> - use a device with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Asset</strong> - use an asset with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Entity View</strong> - use entity view with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Tenant</strong> - use current tenant as target entity to delete relation with.</li>" +
"<li><strong>Customer</strong> - use customer with the specified title as the target entity to delete relation with.</li>" +
"<li><strong>Dashboard</strong> - use a dashboard with the specified title as the target entity to delete relation with.</li>" +
"<li><strong>User</strong> - use a user with the specified email as the target entity to delete relation with.</li>" +
"<li><strong>Edge</strong> - use an edge with the specified name as the target entity to delete relation with.</li></ul>" +
"Output connections: <code>Success</code> - If the relation(s) successfully deleted, otherwise <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeDeleteRelationConfig",
icon = "remove_circle"
icon = "remove_circle",
version = 1
)
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
@Override
protected TbDeleteRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
var deleteRelationNodeConfiguration = TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
if (!deleteRelationNodeConfiguration.isDeleteForSingleEntity()) {
return deleteRelationNodeConfiguration;
}
checkIfConfigEntityTypeIsSupported(deleteRelationNodeConfiguration.getEntityType());
return deleteRelationNodeConfiguration;
}
@Override
@ -58,57 +76,41 @@ public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteR
}
@Override
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return getRelationContainerListenableFuture(ctx, msg, relationType);
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
return Futures.transform(processSingle(ctx, msg, entityContainer, relationType), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
public void onMsg(TbContext ctx, TbMsg msg) {
ListenableFuture<Boolean> deleteResultFuture = config.isDeleteForSingleEntity() ?
Futures.transformAsync(getTargetEntityId(ctx, msg), targetEntityId ->
deleteRelationToSpecificEntity(ctx, msg, targetEntityId), MoreExecutors.directExecutor()) :
deleteRelationsByTypeAndDirection(ctx, msg, ctx.getDbCallbackExecutor());
withCallback(deleteResultFuture, deleted -> {
if (deleted) {
ctx.tellSuccess(msg);
return;
}
ctx.tellFailure(msg, new RuntimeException("Failed to delete relation(s) with originator!"));
},
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<RelationContainer> getRelationContainerListenableFuture(TbContext ctx, TbMsg msg, String relationType) {
if (config.isDeleteForSingleEntity()) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
private ListenableFuture<Boolean> deleteRelationToSpecificEntity(TbContext ctx, TbMsg msg, EntityId targetEntityId) {
EntityId fromId;
EntityId toId;
if (EntitySearchDirection.FROM.equals(config.getDirection())) {
fromId = msg.getOriginator();
toId = targetEntityId;
} else {
return Futures.transform(processList(ctx, msg), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
toId = msg.getOriginator();
fromId = targetEntityId;
}
}
private ListenableFuture<Boolean> processList(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(processListSearchDirection(ctx, msg), entityRelations -> {
if (entityRelations.isEmpty()) {
return Futures.immediateFuture(true);
} else {
List<ListenableFuture<Boolean>> listenableFutureList = new ArrayList<>();
for (EntityRelation entityRelation : entityRelations) {
listenableFutureList.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), entityRelation));
}
return Futures.transformAsync(Futures.allAsList(listenableFutureList), booleans -> {
for (Boolean bool : booleans) {
if (!bool) {
return Futures.immediateFuture(false);
}
var relationType = processPattern(msg, config.getRelationType());
var tenantId = ctx.getTenantId();
var relationService = ctx.getRelationService();
return Futures.transformAsync(relationService.checkRelationAsync(tenantId, fromId, toId, relationType, RelationTypeGroup.COMMON),
relationExists -> {
if (relationExists) {
return relationService.deleteRelationAsync(tenantId, fromId, toId, relationType, RelationTypeGroup.COMMON);
}
return Futures.immediateFuture(true);
}, ctx.getDbCallbackExecutor());
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
return Futures.transformAsync(ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON),
result -> {
if (result) {
return processSingleDeleteRelation(ctx, sdId, relationType);
}
return Futures.immediateFuture(true);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingleDeleteRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON);
}, MoreExecutors.directExecutor());
}
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java

@ -16,10 +16,13 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbDeleteRelationNodeConfiguration> {
private boolean deleteForSingleEntity;
@ -27,11 +30,10 @@ public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionN
@Override
public TbDeleteRelationNodeConfiguration defaultConfiguration() {
TbDeleteRelationNodeConfiguration configuration = new TbDeleteRelationNodeConfiguration();
configuration.setDeleteForSingleEntity(true);
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType("Contains");
configuration.setDeleteForSingleEntity(false);
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityNamePattern("");
configuration.setEntityCacheExpiration(300);
return configuration;
}
}

82
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java

@ -15,14 +15,17 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -34,11 +37,15 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.ACTION,
name = "unassign from customer",
configClazz = TbUnassignFromCustomerNodeConfiguration.class,
nodeDescription = "Unassign Message Originator Entity from Customer",
nodeDetails = "Finds target Entity Customer by Customer name pattern and then unassign Originator Entity from this customer.",
nodeDescription = "Unassign message originator entity from customer",
nodeDetails = "If the message originator is not assigned to any customer, rule node will do nothing. <br><br>" +
"If the incoming message originator is a dashboard, will try to search for the customer by title specified in the configuration. " +
"If customer doesn't exist, the exception will be thrown. Otherwise will unassign the dashboard from retrieved customer.<br><br>" +
"Other entities can be assigned only to one customer, so specified customer title in the configuration will be ignored if the originator isn't a dashboard.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeUnAssignToCustomerConfig",
icon = "remove_circle"
icon = "remove_circle",
version = 1
)
public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode<TbUnassignFromCustomerNodeConfiguration> {
@ -53,48 +60,33 @@ public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode<TbU
}
@Override
protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
EntityType originatorType = msg.getOriginator().getEntityType();
switch (originatorType) {
case DEVICE:
processUnnasignDevice(ctx, msg);
break;
case ASSET:
processUnnasignAsset(ctx, msg);
break;
case ENTITY_VIEW:
processUnassignEntityView(ctx, msg);
break;
case EDGE:
processUnassignEdge(ctx, msg);
break;
case DASHBOARD:
processUnnasignDashboard(ctx, msg, customerId);
break;
default:
ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + originatorType +
"'! Only 'DEVICE', 'ASSET', 'ENTITY_VIEW' or 'DASHBOARD' types are allowed."));
break;
protected ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
var originator = msg.getOriginator();
var originatorType = originator.getEntityType();
var tenantId = ctx.getTenantId();
if (EntityType.DASHBOARD.equals(originatorType)) {
if (StringUtils.isEmpty(config.getCustomerNamePattern())) {
throw new RuntimeException("Failed to unassign dashboard with id '" +
originator.getId() + "' from customer! Customer title should be specified!");
}
var customerIdFuture = getCustomerIdFuture(ctx, msg);
return Futures.transform(customerIdFuture, customerId -> {
ctx.getDashboardService().unassignDashboardFromCustomer(tenantId, new DashboardId(originator.getId()), customerId);
return null;
}, MoreExecutors.directExecutor());
}
return ctx.getDbCallbackExecutor().submit(() -> {
switch (originatorType) {
case ASSET ->
ctx.getAssetService().unassignAssetFromCustomer(tenantId, new AssetId(originator.getId()));
case DEVICE ->
ctx.getDeviceService().unassignDeviceFromCustomer(tenantId, new DeviceId(originator.getId()));
case ENTITY_VIEW ->
ctx.getEntityViewService().unassignEntityViewFromCustomer(tenantId, new EntityViewId(originator.getId()));
case EDGE -> ctx.getEdgeService().unassignEdgeFromCustomer(tenantId, new EdgeId(originator.getId()));
}
return null;
});
}
private void processUnnasignAsset(TbContext ctx, TbMsg msg) {
ctx.getAssetService().unassignAssetFromCustomer(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()));
}
private void processUnnasignDevice(TbContext ctx, TbMsg msg) {
ctx.getDeviceService().unassignDeviceFromCustomer(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()));
}
private void processUnnasignDashboard(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDashboardService().unassignDashboardFromCustomer(ctx.getTenantId(), new DashboardId(msg.getOriginator().getId()), customerId);
}
private void processUnassignEntityView(TbContext ctx, TbMsg msg) {
ctx.getEntityViewService().unassignEntityViewFromCustomer(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()));
}
private void processUnassignEdge(TbContext ctx, TbMsg msg) {
ctx.getEdgeService().unassignEdgeFromCustomer(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId()));
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeConfiguration.java

@ -16,16 +16,17 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbUnassignFromCustomerNodeConfiguration extends TbAbstractCustomerActionNodeConfiguration implements NodeConfiguration<TbUnassignFromCustomerNodeConfiguration> {
@Override
public TbUnassignFromCustomerNodeConfiguration defaultConfiguration() {
TbUnassignFromCustomerNodeConfiguration configuration = new TbUnassignFromCustomerNodeConfiguration();
var configuration = new TbUnassignFromCustomerNodeConfiguration();
configuration.setCustomerNamePattern("");
configuration.setCustomerCacheExpiration(300);
return configuration;
}
}

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java

@ -60,8 +60,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbFilterNodeCheckRelationConfig")
public class TbCheckRelationNode implements TbNode {
private static final String DIRECTION_PROPERTY_NAME = "direction";
private TbCheckRelationNodeConfiguration config;
private EntityId singleEntityId;
@ -114,19 +112,20 @@ public class TbCheckRelationNode implements TbNode {
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
if (fromVersion == 0) {
var newConfigObjectNode = (ObjectNode) oldConfiguration;
if (!newConfigObjectNode.has(DIRECTION_PROPERTY_NAME)) {
throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' doesn't exists in configuration!");
var directionPropertyName = "direction";
if (!newConfigObjectNode.has(directionPropertyName)) {
throw new TbNodeException("property to update: '" + directionPropertyName + "' doesn't exists in configuration!");
}
String direction = newConfigObjectNode.get(DIRECTION_PROPERTY_NAME).asText();
String direction = newConfigObjectNode.get(directionPropertyName).asText();
if (EntitySearchDirection.TO.name().equals(direction)) {
newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.FROM.name());
newConfigObjectNode.put(directionPropertyName, EntitySearchDirection.FROM.name());
return new TbPair<>(true, newConfigObjectNode);
}
if (EntitySearchDirection.FROM.name().equals(direction)) {
newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.TO.name());
newConfigObjectNode.put(directionPropertyName, EntitySearchDirection.TO.name());
return new TbPair<>(true, newConfigObjectNode);
}
throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' has invalid value!");
throw new TbNodeException("property to update: '" + directionPropertyName + "' has invalid value!");
}
return new TbPair<>(false, oldConfiguration);
}

8
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

332
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeTest.java

@ -0,0 +1,332 @@
/**
* Copyright © 2016-2024 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.action;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TbAssignToCustomerNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET,
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private final Device DEVICE = new Device();
private final Asset ASSET = new Asset();
private final EntityView ENTITY_VIEW = new EntityView();
private final Edge EDGE = new Edge();
private final Dashboard DASHBOARD = new Dashboard();
private final TenantId TENANT_ID = new TenantId(UUID.fromString("c818385f-e661-407f-8c52-daf2dddf406d"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("c3570bd0-c0bc-4609-97a4-6f57d7c8b809"));
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() {
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type)));
}
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify() {
return supportedEntityTypes.stream()
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5))));
}
private TbAssignToCustomerNode node;
private TbAssignToCustomerNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbAssignToCustomerNode());
config = new TbAssignToCustomerNodeConfiguration().defaultConfiguration();
}
@Override
protected TbNode getTestNode() {
return node;
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbAssignToCustomerNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty();
assertThat(defaultConfig.isCreateCustomerIfNotExists()).isFalse();
}
@ParameterizedTest
@MethodSource
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) {
// GIVEN
var originator = toOriginator(originatorType);
var msg = getTbMsg(originator);
// WHEN
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
verifyNoInteractions(ctxMock);
verifyNoInteractions(customerServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(eq(TENANT_ID), eq(customerTitle)))
.thenReturn(Futures.immediateFuture(Optional.of(customer)));
Map<EntityType, Consumer<EntityId>> entityTypeToAssignConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToAssignConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerCreatedAndSuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(ctxMock.getSelfId()).thenReturn(RULE_NODE_ID);
config.setCreateCustomerIfNotExists(true);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(eq(TENANT_ID), eq(customerTitle)))
.thenReturn(Futures.immediateFuture(Optional.empty()));
when(customerServiceMock.saveCustomer(any(Customer.class))).thenReturn(customer);
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(ctxMock).enqueue(any(), runnableCaptor.capture(), any());
runnableCaptor.getValue().run();
verify(ctxMock).customerCreatedMsg(any(), eq(RULE_NODE_ID));
verifyMsgSuccess(msg);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerNotFound(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(eq(TENANT_ID), eq(customerTitle)))
.thenReturn(Futures.immediateFuture(Optional.empty()));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!");
verifyNoMoreInteractions(customerServiceMock);
verifyNoMoreInteractions(ctxMock);
}
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() {
return Map.of(
EntityType.DEVICE, id -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.assignDeviceToCustomer(eq(TENANT_ID), (DeviceId) eq(id), any()))
.thenReturn(DEVICE);
},
EntityType.ASSET, id -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.assignAssetToCustomer(eq(TENANT_ID), (AssetId) eq(id), any()))
.thenReturn(ASSET);
},
EntityType.ENTITY_VIEW, id -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.assignEntityViewToCustomer(eq(TENANT_ID), (EntityViewId) eq(id), any()))
.thenReturn(ENTITY_VIEW);
},
EntityType.EDGE, id -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.assignEdgeToCustomer(eq(TENANT_ID), (EdgeId) eq(id), any()))
.thenReturn(EDGE);
},
EntityType.DASHBOARD, id -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.assignDashboardToCustomer(eq(TENANT_ID), (DashboardId) eq(id), any()))
.thenReturn(DASHBOARD);
}
);
}
private void verifyMsgSuccess(TbMsg expectedMsg) {
verify(ctxMock).tellSuccess(eq(expectedMsg));
verify(ctxMock, never()).tellFailure(any(), any());
}
private Customer createCustomer(String customerTitle) {
var customer = new Customer();
customer.setTitle(customerTitle);
customer.setId(new CustomerId(UUID.randomUUID()));
customer.setTenantId(TENANT_ID);
return customer;
}
private TbMsg getTbMsg(EntityId originator) {
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
}
private EntityId toOriginator(EntityType type) {
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString());
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\",\"customerCacheExpiration\":300}",
true,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}",
false,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}")
);
}
}

751
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java

@ -15,193 +15,718 @@
*/
package org.thingsboard.rule.engine.action;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbCreateRelationNodeTest {
public class TbCreateRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private TbCreateRelationNode node;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown() {
return supportedEntityTypes.stream().filter(entityType -> !entityType.equals(EntityType.TENANT)).map(Arguments::of);
}
private static final TenantId tenantId = new TenantId(UUID.fromString("6fc86fc9-b25c-4893-b340-51cf4e101ab2"));
private static final DeviceId deviceId = new DeviceId(UUID.fromString("191ab124-1d8d-4749-97c6-fc84c113c1f5"));
private static final AssetId assetId = new AssetId(UUID.fromString("a47a5867-deab-4333-b845-88cb1695990c"));
private static final CustomerId customerId = new CustomerId(UUID.fromString("4af69229-273d-40de-9fba-f49f87373d23"));
private static final EntityViewId entityViewId = new EntityViewId(UUID.fromString("d4c22c9c-07f5-474d-9d16-b63f0e71f914"));
private static final EdgeId edgeId = new EdgeId(UUID.fromString("7c653959-558d-4661-aac7-c1866eef286b"));
private static final DashboardId dashboardId = new DashboardId(UUID.fromString("6fcfbcb0-21e4-4b0b-a0d6-399ca6959cb2"));
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId)),
Arguments.of(new EntityView(entityViewId)),
Arguments.of(new Edge(edgeId)),
Arguments.of(new Dashboard(dashboardId)),
Arguments.of(new Tenant(tenantId))
);
}
private static Stream<Arguments> givenSupportedEntityTypeToCreateEntityIfNotExists_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId))
);
}
private final DeviceId originatorId = new DeviceId(UUID.fromString("860634b1-8a1e-4693-9ae8-e779c7f5f4da"));
private final RuleNodeId ruleNodeId = new RuleNodeId(UUID.fromString("d05a0491-ee7a-484a-8c1b-91111ef39287"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
@Mock
private TbContext ctx;
private TbContext ctxMock;
@Mock
private AssetService assetService;
private AssetService assetServiceMock;
@Mock
private RelationService relationService;
private TbMsg msg;
private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
private DeviceService deviceServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@Mock
private TbClusterService clusterServiceMock;
@Mock
private RelationService relationServiceMock;
private ListeningExecutor dbExecutor;
private TbCreateRelationNode node;
private TbCreateRelationNodeConfiguration config;
@BeforeEach
public void before() {
dbExecutor = new TestDbCallbackExecutor();
public void setUp() throws TbNodeException {
node = spy(new TbCreateRelationNode());
config = new TbCreateRelationNodeConfiguration().defaultConfiguration();
}
@Test
public void testCreateNewRelation() throws TbNodeException {
init(createRelationNodeConfig());
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbCreateRelationNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getDirection()).isEqualTo(EntitySearchDirection.FROM);
assertThat(defaultConfig.getRelationType()).isEqualTo(EntityRelation.CONTAINS_TYPE);
assertThat(defaultConfig.getEntityNamePattern()).isEqualTo("");
assertThat(defaultConfig.getEntityTypePattern()).isEqualTo(null);
assertThat(defaultConfig.getEntityType()).isEqualTo(null);
assertThat(defaultConfig.isCreateEntityIfNotExists()).isFalse();
assertThat(defaultConfig.isRemoveCurrentRelations()).isFalse();
assertThat(defaultConfig.isChangeOriginatorToRelatedEntity()).isFalse();
}
DeviceId deviceId = new DeviceId(Uuids.timeBased());
@ParameterizedTest
@EnumSource(EntityType.class)
void givenEntityType_whenInit_thenVerifyExceptionThrownIfTypeIsUnsupported(EntityType entityType) {
// GIVEN
config.setEntityType(entityType);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN-THEN
if (unsupportedEntityTypes.contains(entityType)) {
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
} else {
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)).doesNotThrowAnyException();
}
verifyNoInteractions(ctxMock);
}
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
@ParameterizedTest
@MethodSource
void givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown(EntityType entityType) throws TbNodeException {
// GIVEN
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
var mockMethodCallsMap = mockEntityServiceCallsEntityNotFound();
mockMethodCallsMap.get(entityType).run();
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
String validationField = switch (entityType) {
case CUSTOMER, DASHBOARD -> "title";
case USER -> "email";
default -> "name";
};
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage("%s with %s 'EntityName' doesn't exist!", entityType.getNormalName(), validationField);
}
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationCreatedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(false);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyDeleteCurrentRelationCreateNewRelationAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyDeleteCurrentRelationsCreateNewRelationAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS);
}
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(true);
@Test
public void testDeleteCurrentRelationsCreateNewRelation() throws TbNodeException {
init(createRelationNodeConfigWithRemoveCurrentRelations());
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
DeviceId deviceId = new DeviceId(Uuids.timeBased());
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
var firstRelationToDelete = new EntityRelation();
var secondRelationToDelete = new EntityRelation();
var relationsToDelete = List.of(firstRelationToDelete, secondRelationToDelete);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(relationsToDelete));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
EntityRelation relation = new EntityRelation();
when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(Collections.singletonList(relation)));
when(ctx.getRelationService().deleteRelationAsync(any(), eq(relation))).thenReturn(Futures.immediateFuture(true));
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS);
}
// WHEN
node.onMsg(ctxMock, msg);
@Test
public void testCreateNewRelationAndChangeOriginator() throws TbNodeException {
init(createRelationNodeConfigWithChangeOriginator());
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
DeviceId deviceId = new DeviceId(Uuids.timeBased());
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
var entityRelationCaptor = ArgumentCaptor.forClass(EntityRelation.class);
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
verify(relationServiceMock, times(2)).deleteRelationAsync(eq(tenantId), entityRelationCaptor.capture());
assertThat(relationsToDelete).containsExactlyInAnyOrderElementsOf(relationsToDelete);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture());
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationCreatedAndOriginatorChanged(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(true);
config.setRemoveCurrentRelations(false);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
var msgAfterOriginatorChanged = TbMsg.transformMsgOriginator(msg, originatorId);
when(ctxMock.transformMsgOriginator(any(), any())).thenReturn(msgAfterOriginatorChanged);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).transformMsgOriginator(eq(msg), eq(entityId));
verify(ctxMock).tellSuccess(eq(msgAfterOriginatorChanged));
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityTypeToCreateEntityIfNotExists_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationAndEntityCreatedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(true);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(false);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getSelfId()).thenReturn(ruleNodeId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsEnabled();
var entityCreatedMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
mockMethodCallsMap.get(entityType).accept(entity, entityCreatedMsg);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsEnabled();
verifyMethodCallsMap.get(entityType).accept(entity, entityCreatedMsg);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
private Map<EntityType, Consumer<HasId>> mockEntityServiceCallsCreateEntityIfNotExistsDisabled() {
return Map.of(
EntityType.DEVICE, hasId -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
},
EntityType.ASSET, hasId -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(asset));
},
EntityType.CUSTOMER, hasId -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(any(), any())).thenReturn(Futures.immediateFuture(Optional.ofNullable(customer)));
},
EntityType.ENTITY_VIEW, hasId -> {
var entityView = (EntityView) hasId;
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(entityView));
},
EntityType.EDGE, hasId -> {
var edge = (Edge) hasId;
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(edge));
},
EntityType.USER, hasId -> {
var user = (User) hasId;
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmailAsync(any(), any())).thenReturn(Futures.immediateFuture(user));
},
EntityType.DASHBOARD, hasId -> {
var dashboard = (Dashboard) hasId;
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(dashboard));
},
EntityType.TENANT, hasId -> {
}
);
}
assertEquals(assetId, originatorCaptor.getValue());
private Map<EntityType, Runnable> verifyEntityServiceCallsCreateEntityIfNotExistsDisabled() {
return Map.of(
EntityType.DEVICE, () -> {
verify(deviceServiceMock).findDeviceByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(deviceServiceMock);
},
EntityType.ASSET, () -> {
verify(assetServiceMock).findAssetByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, () -> {
verify(customerServiceMock).findCustomerByTenantIdAndTitleAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(customerServiceMock);
},
EntityType.ENTITY_VIEW, () -> {
verify(entityViewServiceMock).findEntityViewByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(entityViewServiceMock);
},
EntityType.EDGE, () -> {
verify(edgeServiceMock).findEdgeByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(edgeServiceMock);
},
EntityType.USER, () -> {
verify(userServiceMock).findUserByTenantIdAndEmailAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(userServiceMock);
},
EntityType.DASHBOARD, () -> {
verify(dashboardServiceMock).findFirstDashboardInfoByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(dashboardServiceMock);
},
EntityType.TENANT, () -> {
}
);
}
public void init(TbCreateRelationNodeConfiguration configuration) throws TbNodeException {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(configuration));
private Map<EntityType, BiConsumer<HasId, TbMsg>> mockEntityServiceCallsCreateEntityIfNotExistsEnabled() {
return Map.of(
EntityType.DEVICE, (hasId, entityCreatedMsg) -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getClusterService()).thenReturn(clusterServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
when(deviceServiceMock.saveDevice(any())).thenReturn(device);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).deviceCreatedMsg(any(), any());
},
EntityType.ASSET, (hasId, entityCreatedMsg) -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
when(assetServiceMock.saveAsset(any())).thenReturn(asset);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).assetCreatedMsg(any(), any());
},
EntityType.CUSTOMER, (hasId, entityCreatedMsg) -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(any(), any())).thenReturn(Futures.immediateFuture(Optional.empty()));
when(customerServiceMock.saveCustomer(any())).thenReturn(customer);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).customerCreatedMsg(any(), any());
}
);
}
when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctx.getRelationService()).thenReturn(relationService);
when(ctx.getAssetService()).thenReturn(assetService);
private Map<EntityType, BiConsumer<HasId, TbMsg>> verifyEntityServiceCallsCreateEntityIfNotExistsEnabled() {
return Map.of(
EntityType.DEVICE, (hasId, entityCreatedMsg) -> {
var device = (Device) hasId;
verify(deviceServiceMock, times(1)).findDeviceByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verify(deviceServiceMock).saveDevice(any());
verify(clusterServiceMock).onDeviceUpdated(eq(device), eq(null));
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(deviceServiceMock, clusterServiceMock);
},
EntityType.ASSET, (hasId, entityCreatedMsg) -> {
verify(assetServiceMock, times(1)).findAssetByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verify(assetServiceMock).saveAsset(any());
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, (hasId, entityCreatedMsg) -> {
verify(customerServiceMock, times(1)).findCustomerByTenantIdAndTitleAsync(eq(tenantId), eq("EntityName"));
verify(customerServiceMock).saveCustomer(any());
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(customerServiceMock);
}
);
}
node = new TbCreateRelationNode();
node.init(ctx, nodeConfiguration);
private Map<EntityType, Runnable> mockEntityServiceCallsEntityNotFound() {
return Map.of(
EntityType.DEVICE, () -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.ASSET, () -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.CUSTOMER, () -> {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(any(), any())).thenReturn(Futures.immediateFuture(Optional.empty()));
},
EntityType.ENTITY_VIEW, () -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.EDGE, () -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.USER, () -> {
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmailAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.DASHBOARD, () -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
}
);
}
private TbCreateRelationNodeConfiguration createRelationNodeConfig() {
TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityCacheExpiration(300);
configuration.setEntityType(EntityType.ASSET.name());
configuration.setEntityNamePattern("${name}");
configuration.setEntityTypePattern("${type}");
configuration.setCreateEntityIfNotExists(false);
configuration.setChangeOriginatorToRelatedEntity(false);
configuration.setRemoveCurrentRelations(false);
return configuration;
private TbMsg getTbMsg(EntityId originator, TbMsgMetaData metaData) {
return TbMsg.newMsg(TbMsgType.NA, originator, metaData, TbMsg.EMPTY_JSON_OBJECT);
}
private TbCreateRelationNodeConfiguration createRelationNodeConfigWithRemoveCurrentRelations() {
TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig();
configuration.setRemoveCurrentRelations(true);
return configuration;
private TbMsgMetaData getMetadataWithNameTemplate() {
var metaData = new TbMsgMetaData();
metaData.putValue("name", "EntityName");
return metaData;
}
private TbCreateRelationNodeConfiguration createRelationNodeConfigWithChangeOriginator() {
TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig();
configuration.setChangeOriginatorToRelatedEntity(true);
return configuration;
@Override
protected TbNode getTestNode() {
return node;
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false,\"entityCacheExpiration\":300}",
true,
"{\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false,\"entityCacheExpiration\":300}",
true,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}"),
// config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}",
false,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}")
);
}
}

605
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java

@ -0,0 +1,605 @@
/**
* Copyright © 2016-2024 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.action;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbDeleteRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown() {
return supportedEntityTypes.stream().filter(entityType -> !entityType.equals(EntityType.TENANT)).map(Arguments::of);
}
private static final TenantId tenantId = new TenantId(UUID.fromString("6fdb457d-0910-401c-8880-abc251e6a1e2"));
private static final DeviceId deviceId = new DeviceId(UUID.fromString("4eef91a7-8865-4c3c-837d-ed6f6577508b"));
private static final AssetId assetId = new AssetId(UUID.fromString("f4fd3b10-3f36-4d46-a162-5e62050774cc"));
private static final CustomerId customerId = new CustomerId(UUID.fromString("ab890af2-3622-41e0-ac94-14d50af84348"));
private static final EntityViewId entityViewId = new EntityViewId(UUID.fromString("39ce8d03-52a3-4aa8-b561-267d1d9d68b5"));
private static final EdgeId edgeId = new EdgeId(UUID.fromString("dc4f9809-b6f9-48f9-8057-2737216cfdf7"));
private static final DashboardId dashboardId = new DashboardId(UUID.fromString("fda72baa-c882-4723-9693-25995dc37bc5"));
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId)),
Arguments.of(new EntityView(entityViewId)),
Arguments.of(new Edge(edgeId)),
Arguments.of(new Dashboard(dashboardId)),
Arguments.of(new Tenant(tenantId))
);
}
private final DeviceId originatorId = new DeviceId(UUID.fromString("574c9840-0885-4d12-be69-f557d7471a78"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@Mock
private RelationService relationServiceMock;
private TbDeleteRelationNode node;
private TbDeleteRelationNodeConfiguration config;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbDeleteRelationNode());
config = new TbDeleteRelationNodeConfiguration().defaultConfiguration();
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbDeleteRelationNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getDirection()).isEqualTo(EntitySearchDirection.FROM);
assertThat(defaultConfig.getRelationType()).isEqualTo(EntityRelation.CONTAINS_TYPE);
assertThat(defaultConfig.getEntityNamePattern()).isEqualTo("");
assertThat(defaultConfig.getEntityTypePattern()).isEqualTo(null);
assertThat(defaultConfig.getEntityType()).isEqualTo(null);
assertThat(defaultConfig.isDeleteForSingleEntity()).isFalse();
}
@ParameterizedTest
@EnumSource(EntityType.class)
void givenEntityType_whenInit_thenVerifyExceptionThrownIfTypeIsUnsupported(EntityType entityType) {
// GIVEN
config.setEntityType(entityType);
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN-THEN
if (unsupportedEntityTypes.contains(entityType)) {
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
} else {
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)).doesNotThrowAnyException();
}
verifyNoInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyEntityNotFoundExceptionThrown(EntityType entityType) throws TbNodeException {
// GIVEN
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
var mockMethodCallsMap = mockEntityServiceCallsEntityNotFound();
mockMethodCallsMap.get(entityType).run();
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
String validationField = switch (entityType) {
case CUSTOMER, DASHBOARD -> "title";
case USER -> "email";
default -> "name";
};
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage("%s with %s 'EntityName' doesn't exist!", entityType.getNormalName(), validationField);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationDeletedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationFailedToDeleteAndOutMsgFailure(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellSuccess(any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!");
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationNotFoundAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
if (entityType.equals(EntityType.TENANT)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
}
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
if (entityType.equals(EntityType.TENANT)) {
verify(ctxMock).getDbCallbackExecutor();
}
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationsDeletedAndOutMsgSuccess() throws TbNodeException {
// GIVEN
config.setEntityType(EntityType.DEVICE);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationFailedToDeleteAndOutMsgFailure() throws TbNodeException {
// GIVEN
config.setEntityType(EntityType.DEVICE);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellSuccess(any());
verifyNoMoreInteractions(ctxMock, relationServiceMock);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!");
}
private Map<EntityType, Runnable> mockEntityServiceCallsEntityNotFound() {
return Map.of(
EntityType.DEVICE, () -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.ASSET, () -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.CUSTOMER, () -> {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(any(), any())).thenReturn(Futures.immediateFuture(Optional.empty()));
},
EntityType.ENTITY_VIEW, () -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.EDGE, () -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.USER, () -> {
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmailAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
},
EntityType.DASHBOARD, () -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(null));
}
);
}
private Map<EntityType, Consumer<HasId>> mockEntityServiceCalls() {
return Map.of(
EntityType.DEVICE, hasId -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
},
EntityType.ASSET, hasId -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(asset));
},
EntityType.CUSTOMER, hasId -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(any(), any())).thenReturn(Futures.immediateFuture(Optional.ofNullable(customer)));
},
EntityType.ENTITY_VIEW, hasId -> {
var entityView = (EntityView) hasId;
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(entityView));
},
EntityType.EDGE, hasId -> {
var edge = (Edge) hasId;
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(edge));
},
EntityType.USER, hasId -> {
var user = (User) hasId;
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmailAsync(any(), any())).thenReturn(Futures.immediateFuture(user));
},
EntityType.DASHBOARD, hasId -> {
var dashboard = (Dashboard) hasId;
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndNameAsync(any(), any())).thenReturn(Futures.immediateFuture(dashboard));
},
EntityType.TENANT, hasId -> {
// do nothing. tenantId returned by ctxMock.
}
);
}
private Map<EntityType, Consumer<HasId>> verifyEntityServiceCalls() {
return Map.of(
EntityType.DEVICE, hasId -> {
verify(deviceServiceMock).findDeviceByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(deviceServiceMock);
},
EntityType.ASSET, hasId -> {
verify(assetServiceMock).findAssetByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, hasId -> {
verify(customerServiceMock).findCustomerByTenantIdAndTitleAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(customerServiceMock);
},
EntityType.ENTITY_VIEW, hasId -> {
verify(entityViewServiceMock).findEntityViewByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(entityViewServiceMock);
},
EntityType.EDGE, hasId -> {
verify(edgeServiceMock).findEdgeByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(edgeServiceMock);
},
EntityType.USER, hasId -> {
verify(userServiceMock).findUserByTenantIdAndEmailAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(userServiceMock);
},
EntityType.DASHBOARD, hasId -> {
verify(dashboardServiceMock).findFirstDashboardInfoByTenantIdAndNameAsync(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(dashboardServiceMock);
},
EntityType.TENANT, hasId -> {
}
);
}
private TbMsg getTbMsg(EntityId originator, TbMsgMetaData metaData) {
return TbMsg.newMsg(TbMsgType.NA, originator, metaData, TbMsg.EMPTY_JSON_OBJECT);
}
private TbMsgMetaData getMetadataWithNameTemplate() {
var metaData = new TbMsgMetaData();
metaData.putValue("name", "EntityName");
return metaData;
}
@Override
protected TbNode getTestNode() {
return node;
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}",
true,
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}",
true,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"),
// config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}",
false,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}")
);
}
}

320
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java

@ -0,0 +1,320 @@
/**
* Copyright © 2016-2024 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.action;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TbUnassignFromCustomerNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET,
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private final Device DEVICE = new Device();
private final Asset ASSET = new Asset();
private final EntityView ENTITY_VIEW = new EntityView();
private final Edge EDGE = new Edge();
private final Dashboard DASHBOARD = new Dashboard();
private final TenantId TENANT_ID = new TenantId(UUID.fromString("06fcc15f-2677-436d-a1cb-7754bd0bcccf"));
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() {
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type)));
}
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg() {
return supportedEntityTypes.stream()
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5))));
}
private TbUnassignFromCustomerNode node;
private TbUnassignFromCustomerNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbUnassignFromCustomerNode());
config = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration();
}
@Override
protected TbNode getTestNode() {
return node;
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty();
}
@ParameterizedTest
@MethodSource
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) {
// GIVEN
var originator = toOriginator(originatorType);
var msg = getTbMsg(originator);
// WHEN
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
verifyNoInteractions(ctxMock);
verifyNoInteractions(customerServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
if (!type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
}
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
// we search for the customer only if incoming message originator is dashboard.
if (type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(eq(TENANT_ID), eq(customerTitle)))
.thenReturn(Futures.immediateFuture(Optional.of(customer)));
}
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerSearchedAndNotFoundOnlyForDashboardOriginator(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
if (!type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
}
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
// we search for the customer only if incoming message originator is dashboard.
if (type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitleAsync(eq(TENANT_ID), eq(customerTitle)))
.thenReturn(Futures.immediateFuture(Optional.empty()));
// DASHBOARD WHEN
node.onMsg(ctxMock, msg);
// DASHBOARD THEN
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!");
verifyNoMoreInteractions(customerServiceMock);
verifyNoMoreInteractions(ctxMock);
return;
}
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// OTHER TYPES WHEN
node.onMsg(ctxMock, msg);
// OTHER TYPES THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() {
return Map.of(
EntityType.DEVICE, id -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.unassignDeviceFromCustomer(eq(TENANT_ID), (DeviceId) eq(id)))
.thenReturn(DEVICE);
},
EntityType.ASSET, id -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.unassignAssetFromCustomer(eq(TENANT_ID), (AssetId) eq(id)))
.thenReturn(ASSET);
},
EntityType.ENTITY_VIEW, id -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.unassignEntityViewFromCustomer(eq(TENANT_ID), (EntityViewId) eq(id)))
.thenReturn(ENTITY_VIEW);
},
EntityType.EDGE, id -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.unassignEdgeFromCustomer(eq(TENANT_ID), (EdgeId) eq(id)))
.thenReturn(EDGE);
},
EntityType.DASHBOARD, id -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.unassignDashboardFromCustomer(eq(TENANT_ID), (DashboardId) eq(id), any()))
.thenReturn(DASHBOARD);
}
);
}
private void verifyMsgSuccess(TbMsg expectedMsg) {
verify(ctxMock).tellSuccess(eq(expectedMsg));
verify(ctxMock, never()).tellFailure(any(), any());
}
private Customer createCustomer(String customerTitle) {
var customer = new Customer();
customer.setTitle(customerTitle);
customer.setId(new CustomerId(UUID.randomUUID()));
customer.setTenantId(TENANT_ID);
return customer;
}
private TbMsg getTbMsg(EntityId originator) {
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
}
private static EntityId toOriginator(EntityType type) {
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString());
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"customerCacheExpiration\":300}",
true,
"{\"customerNamePattern\":\"\"}"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\"}",
false,
"{\"customerNamePattern\":\"\"}")
);
}
}

37
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java

@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.ArgumentCaptor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.AssetId;
@ -42,6 +45,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -52,16 +56,17 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TbCheckRelationNodeTest {
class TbCheckRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID());
private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
private final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID());
private final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
private TbCheckRelationNode node;
@ -77,7 +82,7 @@ class TbCheckRelationNodeTest {
when(ctx.getRelationService()).thenReturn(relationService);
when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
node = new TbCheckRelationNode();
node = spy(new TbCheckRelationNode());
}
@AfterEach
@ -310,4 +315,24 @@ class TbCheckRelationNodeTest {
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"checkForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}",
true,
"{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}",
true,
"{\"checkForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

23
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java

@ -89,13 +89,13 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbMathNodeTest {
static final int RULE_DISPATCHER_POOL_SIZE = 2;
static final int DB_CALLBACK_POOL_SIZE = 3;
static final int RULE_DISPATCHER_POOL_SIZE = 3;
static final int DB_CALLBACK_POOL_SIZE = 4;
static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
private final EntityId originator = DeviceId.fromString("ccd71696-0586-422d-940e-755a41ec3b0d");
private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("e7f46b23-0c7d-42f5-9b06-fc35ab17af8a"));
@Mock(lenient = true)
@Mock(strictness = Mock.Strictness.LENIENT)
private TbContext ctx;
@Mock
private AttributesService attributesService;
@ -122,8 +122,9 @@ public class TbMathNodeTest {
@AfterEach
public void after() {
ruleEngineDispatcherExecutor.executor().shutdownNow();
dbCallbackExecutor.executor().shutdownNow();
// shutdownNow makes some tests flaky
ruleEngineDispatcherExecutor.destroy();
dbCallbackExecutor.destroy();
}
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) {
@ -538,7 +539,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture());
Mockito.verify(ctx, timeout(TIMEOUT)).tellFailure(eq(msg), tCaptor.capture());
assertNotNull(tCaptor.getValue().getMessage());
}
@ -553,7 +554,7 @@ public class TbMathNodeTest {
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture());
Mockito.verify(ctx, timeout(TIMEOUT)).tellFailure(eq(msg), tCaptor.capture());
assertNotNull(tCaptor.getValue().getMessage());
}
@ -669,11 +670,11 @@ public class TbMathNodeTest {
// submit slow msg may block all rule engine dispatcher threads
slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg)));
// wait until dispatcher threads started with all slowMsg
verify(node, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains));
verify(node, new Timeout(TIMEOUT, times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains));
slowProcessingLatch.countDown();
verify(ctx, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).tellFailure(any(), any());
verify(ctx, new Timeout(TIMEOUT, times(slowMsgList.size()))).tellFailure(any(), any());
verify(ctx, never()).tellSuccess(any());
}
@ -713,7 +714,7 @@ public class TbMathNodeTest {
.toList();
ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight()
.onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}"))));
ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(5000)).onMsg(eq(ctxNode.getLeft()), any()));
ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(TIMEOUT)).onMsg(eq(ctxNode.getLeft()), any()));
processingLatch.countDown();
SoftAssertions softly = new SoftAssertions();
@ -721,7 +722,7 @@ public class TbMathNodeTest {
final TbContext ctx = ctxNode.getLeft();
final String resultKey = ctxNode.getMiddle();
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx, timeout(5000)).tellSuccess(msgCaptor.capture());
verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture());
TbMsg resultMsg = msgCaptor.getValue();
assertThat(resultMsg).as("result msg non null for result key " + resultKey).isNotNull();

5
transport/coap/pom.xml

@ -69,11 +69,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

5
transport/http/pom.xml

@ -69,11 +69,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

5
transport/lwm2m/pom.xml

@ -103,11 +103,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

5
transport/mqtt/pom.xml

@ -69,11 +69,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

2
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -115,7 +115,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px'));
this.columns.push(
new EntityLinkTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
(entity) => entity.originatorName,
(entity) => this.utilsService.customTranslation(entity.originatorName, entity.originatorName),
(entity) => getEntityDetailsPageURL(entity.originator.id, entity.originator.entityType as EntityType)));
this.columns.push(
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%',

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

@ -296,7 +296,7 @@
[(opened)]="rightLayoutOpened">
<tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.right.layoutCtx"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
@ -312,7 +312,7 @@
height: mainLayoutSize.height}">
<tb-dashboard-layout
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="layouts.main.layoutCtx"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"

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

@ -320,6 +320,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
set rightLayoutOpened(rightLayoutOpened: boolean) {
}
get mobileDisplayRightLayoutFirst(): boolean {
return this.isMobile && this.layouts.right.layoutCtx.gridSettings?.mobileDisplayLayoutFirst;
}
set mobileDisplayRightLayoutFirst(mobileDisplayRightLayoutFirst: boolean) {
}
@ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent;
@ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;

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

@ -159,25 +159,30 @@
</mat-select>
</mat-form-field>
</fieldset>
<fieldset class="fields-group" fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" fxLayoutGap="8px">
<fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">
<legend class="group-title" translate>dashboard.mobile-layout</legend>
<mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
<mat-slide-toggle *ngIf="isRightLayout" fxFlex formControlName="mobileDisplayLayoutFirst" style="display: block;">
{{ 'dashboard.display-first-in-mobile-view' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.mobile-row-height</mat-label>
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
max="200" required>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
{{ 'dashboard.mobile-row-height-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
{{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" fxLayoutGap="8px" >
<mat-slide-toggle fxFlex formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.mobile-row-height</mat-label>
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
max="200" required>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
{{ 'dashboard.mobile-row-height-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
{{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
</div>
</fieldset>
</div>
</fieldset>

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

Loading…
Cancel
Save