Browse Source

Merge pull request #10527 from thingsboard/feature/rule-node-caches-replacement

Replacement of local cache in rule nodes with cached entity service APIs
pull/10698/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
c8bc0f6793
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  2. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  5. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  6. 60
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  7. 6
      application/src/main/resources/thingsboard.yml
  8. 27
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  9. 13
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  10. 2
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  11. 13
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java
  12. 42
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheKey.java
  13. 33
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCaffeineCache.java
  14. 35
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerRedisCache.java
  15. 21
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheEvictEvent.java
  16. 42
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheKey.java
  17. 33
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCaffeineCache.java
  18. 35
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserRedisCache.java
  19. 5
      common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java
  20. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  21. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  22. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  23. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  24. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  25. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  26. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  27. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  28. 19
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  29. 26
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
  30. 105
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  31. 21
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  32. 19
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  33. 21
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  34. 15
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  35. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  36. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java
  37. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java
  38. 5
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  39. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
  40. 20
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
  41. 75
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  42. 2
      dao/src/main/resources/sql/schema-entities.sql
  43. 86
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java
  44. 43
      dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java
  45. 29
      dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java
  46. 6
      dao/src/test/resources/application-test.properties
  47. 144
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java
  48. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNodeConfiguration.java
  49. 422
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  50. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNodeConfiguration.java
  51. 79
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java
  52. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeConfiguration.java
  53. 254
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
  54. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeConfiguration.java
  55. 110
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
  56. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
  57. 82
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java
  58. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeConfiguration.java
  59. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  60. 332
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeTest.java
  61. 751
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java
  62. 605
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java
  63. 320
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java
  64. 37
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java

17
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
$$

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();

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

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)));

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityContainer.java → common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java

@ -13,16 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
package org.thingsboard.server.cache.customer;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
@Data
public class EntityContainer {
private EntityId entityId;
private EntityType entityType;
import org.thingsboard.server.common.data.id.TenantId;
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";

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!");

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;

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

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);
}

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;
}
}

Loading…
Cancel
Save