Browse Source

extended name conflict strategy to include name suffix separator

pull/14118/head
dashevchenko 10 months ago
parent
commit
720eab396c
  1. 3
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 15
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  4. 29
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  5. 15
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  6. 4
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  8. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java
  9. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  11. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  12. 18
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  13. 15
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  14. 18
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  15. 19
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  16. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  17. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  18. 23
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java
  19. 8
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/Dao.java
  21. 1
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
  22. 7
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  23. 26
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  24. 1
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  25. 11
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  26. 12
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  27. 10
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  28. 8
      dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java
  29. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
  30. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  31. 1
      dao/src/main/resources/sql/schema-entities.sql

3
application/src/main/data/upgrade/basic/schema_update.sql

@ -46,3 +46,6 @@ WHERE NOT (
);
-- UPDATE TENANT PROFILE CONFIGURATION END
ALTER TABLE entity_view ADD CONSTRAINT entity_view_name_unq_key UNIQUE (tenant_id, name);

15
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -34,6 +34,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
@ -139,13 +140,17 @@ public class AssetController extends BaseController {
@RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody
public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset,
@Parameter(description = "Optional value of name conflict strategy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL strategy is applied. FAIL strategy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY strategy appends a numerical suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "nameConflictStrategy", defaultValue = "FAIL") NameConflictStrategy nameConflictStrategy) throws Exception {
@Parameter(description = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "policy", defaultValue = "FAIL") NameConflictPolicy policy,
@Parameter(description = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for customer asset 'Office A', " +
"created asset will have name like 'Office A-7fsh4f'.")
@RequestParam(name = "separator", defaultValue = "_") String separator) throws Exception {
asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, nameConflictStrategy, getCurrentUser());
return tbAssetService.save(asset, new NameConflictStrategy(policy, separator), getCurrentUser());
}
@ApiOperation(value = "Delete asset (deleteAsset)",

15
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -130,13 +131,17 @@ public class CustomerController extends BaseController {
@RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody
public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer,
@Parameter(description = "Optional value of name conflict strategy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL strategy is applied. FAIL strategy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY strategy appends a numerical suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "nameConflictStrategy", defaultValue = "FAIL") NameConflictStrategy nameConflictStrategy) throws Exception {
@Parameter(description = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "policy", defaultValue = "FAIL") NameConflictPolicy policy,
@Parameter(description = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for customer name 'Customer A', " +
"created customer will have name like 'Customer A-7fsh4f'.")
@RequestParam(name = "separator", defaultValue = "_") String separator) throws Exception {
customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER);
return tbCustomerService.save(customer, getCurrentUser());
return tbCustomerService.save(customer, new NameConflictStrategy(policy, separator), getCurrentUser());
}
@ApiOperation(value = "Delete Customer (deleteCustomer)",

29
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
@ -180,17 +181,21 @@ public class DeviceController extends BaseController {
@Parameter(description = "Optional value of the device credentials to be used during device creation. " +
"If omitted, access token will be auto-generated.")
@RequestParam(name = "accessToken", required = false) String accessToken,
@Parameter(description = "Optional value of name conflict strategy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL strategy is applied. FAIL strategy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY strategy appends a numerical suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "nameConflictStrategy", defaultValue = "FAIL") NameConflictStrategy nameConflictStrategy) throws Exception {
@Parameter(description = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "policy", defaultValue = "FAIL") NameConflictPolicy policy,
@Parameter(description = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for device name 'thermostat', " +
"created device will have name like 'thermostat-7fsh4f'.")
@RequestParam(name = "separator", defaultValue = "_") String separator) throws Exception {
device.setTenantId(getCurrentUser().getTenantId());
if (device.getId() != null) {
checkDeviceId(device.getId(), Operation.WRITE);
} else {
checkEntity(null, device, Resource.DEVICE);
}
return tbDeviceService.save(device, accessToken, nameConflictStrategy, getCurrentUser());
return tbDeviceService.save(device, accessToken, new NameConflictStrategy(policy, separator), getCurrentUser());
}
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
@ -216,15 +221,19 @@ public class DeviceController extends BaseController {
@ResponseBody
public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.")
@Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials,
@Parameter(description = "Optional value of name conflict strategy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL strategy is applied. FAIL strategy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY strategy appends a numerical suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "nameConflictStrategy", defaultValue = "FAIL") NameConflictStrategy nameConflictStrategy) throws ThingsboardException {
@Parameter(description = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "policy", defaultValue = "FAIL") NameConflictPolicy policy,
@Parameter(description = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for device name 'thermostat', " +
"created device will have name like 'thermostat-7fsh4f'.")
@RequestParam(name = "separator", defaultValue = "_") String separator) throws ThingsboardException {
Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE);
return tbDeviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy, getCurrentUser());
return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(policy, separator), getCurrentUser());
}
@ApiOperation(value = "Delete device (deleteDevice)",

15
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
@ -130,10 +131,14 @@ public class EntityViewController extends BaseController {
public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView,
@Parameter(description = "Optional value of name conflict strategy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL strategy is applied. FAIL strategy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY strategy appends a numerical suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "nameConflictStrategy", defaultValue = "FAIL") NameConflictStrategy nameConflictStrategy) throws Exception {
@Parameter(description = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
"If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
"UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.")
@RequestParam(name = "policy", defaultValue = "FAIL") NameConflictPolicy policy,
@Parameter(description = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity view name 'Device A', " +
"created customer will have name like 'Device A-7fsh4f'.")
@RequestParam(name = "separator", defaultValue = "_") String separator) throws Exception {
entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null;
if (entityView.getId() == null) {
@ -142,7 +147,7 @@ public class EntityViewController extends BaseController {
} else {
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
}
return tbEntityViewService.save(entityView, existingEntityView, nameConflictStrategy, getCurrentUser());
return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(policy, separator), getCurrentUser());
}
@ApiOperation(value = "Delete entity view (deleteEntityView)",

4
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault;
@ -38,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService;
import java.util.Map;
import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ -74,6 +76,6 @@ public class Lwm2mController extends BaseController {
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), NameConflictStrategy.FAIL);
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator());
}
}

3
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
@ -129,7 +130,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
}
device.setDeviceProfileId(deviceProfile.getId());
return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.FAIL, user);
return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.DEFAULT, user);
}
@Override

5
application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -26,6 +27,10 @@ public interface SimpleTbEntityService<T> {
T save(T entity, SecurityUser user) throws Exception;
default T save(T entity, NameConflictStrategy nameConflictPolicy, SecurityUser user) throws Exception {
return save(entity, null);
}
void delete(T entity, User user);
}

5
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -39,6 +39,11 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
private final AssetService assetService;
@Override
public Asset save(Asset asset, User user) throws Exception {
return save(asset, NameConflictStrategy.DEFAULT, user);
}
@Override
public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;

2
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -26,6 +26,8 @@ import org.thingsboard.server.common.data.id.TenantId;
public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception;
Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void delete(Asset asset, User user);

8
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
@Override
public Customer save(Customer customer, SecurityUser user) throws Exception {
return save(customer, NameConflictStrategy.DEFAULT, user);
}
@Override
public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId();
try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy));
autoCommit(user, savedCustomer.getId());
logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer;

18
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -1080,6 +1080,24 @@ public class AssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId);
}
@Test
public void testSaveAssetWithUniquifyStrategy() throws Exception {
Asset asset = new Asset();
asset.setName("My asset");
asset.setType("default");
doPost("/api/asset", asset, Asset.class);
doPost("/api/asset", asset).andExpect(status().isBadRequest());
doPost("/api/asset?policy=FAIL", asset).andExpect(status().isBadRequest());
Asset secondAsset = doPost("/api/asset?policy=UNIQUIFY", asset, Asset.class);
assertThat(secondAsset.getName()).startsWith("My asset_");
Asset thirdAsset = doPost("/api/asset?policy=UNIQUIFY&separator=-", asset, Asset.class);
assertThat(thirdAsset.getName()).startsWith("My asset-");
}
private Asset createAsset(String name) {
Asset asset = new Asset();
asset.setName(name);

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

@ -462,6 +462,21 @@ public class CustomerControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId);
}
@Test
public void testSaveCustomerWithUniquifyStrategy() throws Exception {
Customer customer = new Customer();
customer.setTitle("My customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
doPost("/api/customer?policy=FAIL", customer).andExpect(status().isBadRequest());
Customer secondCustomer = doPost("/api/customer?policy=UNIQUIFY", customer, Customer.class);
assertThat(secondCustomer.getName()).startsWith("My customer_");
Customer thirdCustomer = doPost("/api/customer?policy=UNIQUIFY&separator=-", customer, Customer.class);
assertThat(thirdCustomer.getName()).startsWith("My customer-");
}
private Customer createCustomer(String title) {
Customer customer = new Customer();
customer.setTitle(title);

18
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -1608,6 +1608,24 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(device.getVersion()).isEqualTo(3);
}
@Test
public void testSaveDeviceWithUniquifyStrategy() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
doPost("/api/device", device).andExpect(status().isBadRequest());
doPost("/api/device?policy=FAIL", device).andExpect(status().isBadRequest());
Device secondDevice = doPost("/api/device?policy=UNIQUIFY", device, Device.class);
assertThat(secondDevice.getName()).startsWith("My device_");
Device thirdDevice = doPost("/api/device?policy=UNIQUIFY&separator=-", device, Device.class);
assertThat(thirdDevice.getName()).startsWith("My device-");
}
private Device createDevice(String name) {
Device device = new Device();
device.setName(name);

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

@ -853,4 +853,23 @@ public class EntityViewControllerTest extends AbstractControllerTest {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId);
}
@Test
public void testSaveEntityViewWithUniquifyStrategy() throws Exception {
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(tenantId);
view.setType("default");
view.setName("Test device view");
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
doPost("/api/entityView?policy=FAIL", view).andExpect(status().isBadRequest());
EntityView secondView = doPost("/api/entityView?policy=UNIQUIFY", view, EntityView.class);
assertThat(secondView.getName()).startsWith("Test device view_");
EntityView thirdView = doPost("/api/entityView?policy=UNIQUIFY&separator=-", view, EntityView.class);
assertThat(thirdView.getName()).startsWith("Test device view-");
}
}

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

@ -26,15 +26,12 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
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.entity.EntityDaoService;
import java.util.List;
import java.util.Optional;
public interface AssetService extends EntityDaoService {

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

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -37,6 +38,8 @@ public interface CustomerService extends EntityDaoService {
Customer saveCustomer(Customer customer);
Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy);
void deleteCustomer(TenantId tenantId, CustomerId customerId);
Customer findOrCreatePublicCustomer(TenantId tenantId);

23
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
public enum NameConflictPolicy {
FAIL,
UNIQUIFY;
}

8
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java

@ -15,9 +15,11 @@
*/
package org.thingsboard.server.common.data;
public enum NameConflictStrategy {
import io.swagger.v3.oas.annotations.media.Schema;
FAIL,
UNIQUIFY;
@Schema
public record NameConflictStrategy(NameConflictPolicy policy, String separator) {
public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null);
}

4
dao/src/main/java/org/thingsboard/server/dao/Dao.java

@ -33,7 +33,9 @@ public interface Dao<T> {
ListenableFuture<T> findByIdAsync(TenantId tenantId, UUID id);
EntityInfo findEntityInfoByName(TenantId tenantId, String name);
default EntityInfo findEntityInfoByName(TenantId tenantId, String name) {
throw new UnsupportedOperationException();
}
boolean existsById(TenantId tenantId, UUID id);

1
dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java

@ -243,5 +243,4 @@ public interface AssetDao extends Dao<Asset>, TenantEntityDao<Asset>, Exportable
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink);
EntityInfo findEntityInfoByName(TenantId tenantId, String name);
}

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

@ -26,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.StringUtils;
@ -154,7 +155,7 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset saveAsset(Asset asset, boolean doValidate) {
return saveAsset(asset, doValidate, NameConflictStrategy.FAIL);
return saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT);
}
private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
@ -165,8 +166,8 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
} else if (asset.getId() != null) {
oldAsset = findAssetById(asset.getTenantId(), asset.getId());
}
if (nameConflictStrategy == NameConflictStrategy.UNIQUIFY) {
uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET);
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY) {
uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy);
}
AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null);
Asset savedAsset;

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

@ -28,6 +28,8 @@ 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.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -139,19 +141,29 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Override
@Transactional
public Customer saveCustomer(Customer customer) {
return saveCustomer(customer, true);
return saveCustomer(customer, true, NameConflictStrategy.DEFAULT);
}
@Override
@Transactional
public Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy) {
return saveCustomer(customer, true, nameConflictStrategy);
}
private Customer saveCustomer(Customer customer, boolean doValidate) {
return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT);
}
private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveCustomer [{}]", customer);
String oldCustomerTitle = null;
Customer oldCustomer = null;
if (doValidate) {
Customer oldCustomer = customerValidator.validate(customer, Customer::getTenantId);
if (oldCustomer != null) {
oldCustomerTitle = oldCustomer.getTitle();
}
oldCustomer = customerValidator.validate(customer, Customer::getTenantId);
}
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY) {
uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy);
}
var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle);
var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomer != null ? oldCustomer.getTitle() : null);
try {
Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer);
if (!savedCustomer.isPublic()) {

1
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -236,5 +236,4 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao<Device>, Exporta
PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
EntityInfo findEntityInfoByName(TenantId tenantId, String name);
}

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

@ -36,10 +36,10 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.StringUtils;
@ -90,7 +90,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -190,7 +189,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
@Transactional
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) {
return this.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.FAIL);
return this.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.DEFAULT);
}
@Transactional
@ -213,7 +212,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
}
private Device doSaveDevice(Device device, String accessToken, boolean doValidate) {
return doSaveDevice(device, accessToken, doValidate, NameConflictStrategy.FAIL);
return doSaveDevice(device, accessToken, doValidate, NameConflictStrategy.DEFAULT);
}
private Device doSaveDevice(Device device, String accessToken, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
@ -236,8 +235,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
} else if (device.getId() != null) {
oldDevice = findDeviceById(device.getTenantId(), device.getId());
}
if (nameConflictStrategy == NameConflictStrategy.UNIQUIFY) {
uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE);
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY) {
uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE, nameConflictStrategy);
}
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null);
try {

12
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -22,21 +22,19 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.Dao;
@ -169,18 +167,18 @@ public abstract class AbstractEntityService {
return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes));
}
protected <E extends HasId<?> & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer<String> setName, EntityType entityType) {
protected <E extends HasId<?> & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer<String> setName, EntityType entityType, NameConflictStrategy nameConflictStrategy) {
Dao<?> dao = entityDaoRegistry.getDao(entityType);
EntityInfo existingEntity = dao.findEntityInfoByName(entity.getTenantId(), entity.getName());
if (existingEntity != null && (oldEntity == null || !existingEntity.getId().equals(oldEntity.getId()))) {
int suffix = 1;
String suffix = StringUtils.randomAlphanumeric(6);
while (true) {
String newName = entity.getName() + "-" + suffix;
String newName = entity.getName() + nameConflictStrategy.separator() + suffix;
if (dao.findEntityInfoByName(entity.getTenantId(), newName) == null) {
setName.accept(newName);
break;
}
suffix++;
suffix = StringUtils.randomAlphanumeric(6);
}
}
}

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

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
@ -118,7 +119,7 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
@Override
public EntityView saveEntityView(EntityView entityView, boolean doValidate) {
return saveEntityView(entityView, doValidate, NameConflictStrategy.FAIL);
return saveEntityView(entityView, doValidate, NameConflictStrategy.DEFAULT);
}
private EntityView saveEntityView(EntityView entityView, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
@ -129,8 +130,8 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
} else if (entityView.getId() != null) {
old = findEntityViewById(entityView.getTenantId(), entityView.getId(), false);
}
if (nameConflictStrategy == NameConflictStrategy.FAIL) {
uniquifyEntityName(entityView, old, entityView::setName, EntityType.ENTITY_VIEW);
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY) {
uniquifyEntityName(entityView, old, entityView::setName, EntityType.ENTITY_VIEW, nameConflictStrategy);
}
try {
EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView);
@ -140,7 +141,8 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return saved;
} catch (Exception t) {
checkConstraintViolation(t,
"entity_view_external_id_unq_key", "Entity View with such external id already exists!");
"entity_view_external_id_unq_key", "Entity View with such external id already exists!",
"entity_view_name_unq_key", "Entity View with such name already exists!");
throw t;
}
}

8
dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java

@ -37,14 +37,6 @@ public class EntityViewDataValidator extends DataValidator<EntityView> {
private final TenantService tenantService;
private final CustomerDao customerDao;
@Override
protected void validateCreate(TenantId tenantId, EntityView entityView) {
entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName())
.ifPresent(e -> {
throw new DataValidationException("Entity view with such name already exists!");
});
}
@Override
protected EntityView validateUpdate(TenantId tenantId, EntityView entityView) {
var opt = entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName());

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

@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edqs.fields.CustomerFields;
import org.thingsboard.server.common.data.id.CustomerId;
@ -117,6 +118,11 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
return customerRepository.findNextBatch(id, Limit.of(batchSize));
}
@Override
public EntityInfo findEntityInfoByName(TenantId tenantId, String name) {
return customerRepository.findEntityInfoByName(tenantId.getId(), name);
}
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;

6
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
@ -230,6 +231,11 @@ public class JpaEntityViewDao extends JpaAbstractDao<EntityViewEntity, EntityVie
return entityViewRepository.findNextBatch(id, Limit.of(batchSize));
}
@Override
public EntityInfo findEntityInfoByName(TenantId tenantId, String name) {
return entityViewRepository.findEntityInfoByName(tenantId.getId(), name);
}
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;

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

@ -551,6 +551,7 @@ CREATE TABLE IF NOT EXISTS entity_view (
additional_info varchar,
external_id uuid,
version BIGINT DEFAULT 1,
CONSTRAINT entity_view_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id)
);

Loading…
Cancel
Save