From 720eab396c76dc836eeddc4c0cadd6a7da366114 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 7 Oct 2025 17:20:11 +0300 Subject: [PATCH] extended name conflict strategy to include name suffix separator --- .../main/data/upgrade/basic/schema_update.sql | 3 ++ .../server/controller/AssetController.java | 15 ++++++---- .../server/controller/CustomerController.java | 15 ++++++---- .../server/controller/DeviceController.java | 29 ++++++++++++------- .../controller/EntityViewController.java | 15 ++++++---- .../server/controller/Lwm2mController.java | 4 ++- .../device/DeviceBulkImportService.java | 3 +- .../entitiy/SimpleTbEntityService.java | 5 ++++ .../entitiy/asset/DefaultTbAssetService.java | 5 ++++ .../service/entitiy/asset/TbAssetService.java | 2 ++ .../customer/DefaultTbCustomerService.java | 8 ++++- .../controller/AssetControllerTest.java | 18 ++++++++++++ .../controller/CustomerControllerTest.java | 15 ++++++++++ .../controller/DeviceControllerTest.java | 18 ++++++++++++ .../controller/EntityViewControllerTest.java | 19 ++++++++++++ .../server/dao/asset/AssetService.java | 3 -- .../server/dao/customer/CustomerService.java | 3 ++ .../common/data/NameConflictPolicy.java | 23 +++++++++++++++ .../common/data/NameConflictStrategy.java | 8 +++-- .../java/org/thingsboard/server/dao/Dao.java | 4 ++- .../server/dao/asset/AssetDao.java | 1 - .../server/dao/asset/BaseAssetService.java | 7 +++-- .../dao/customer/CustomerServiceImpl.java | 26 ++++++++++++----- .../server/dao/device/DeviceDao.java | 1 - .../server/dao/device/DeviceServiceImpl.java | 11 ++++--- .../dao/entity/AbstractEntityService.java | 12 ++++---- .../dao/entityview/EntityViewServiceImpl.java | 10 ++++--- .../validator/EntityViewDataValidator.java | 8 ----- .../dao/sql/customer/JpaCustomerDao.java | 6 ++++ .../dao/sql/entityview/JpaEntityViewDao.java | 6 ++++ .../main/resources/sql/schema-entities.sql | 1 + 31 files changed, 232 insertions(+), 72 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 0add4c0545..3986a3c222 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/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); + diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 9cade90860..15fcfa84ff 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/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)", diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 5c4acb6dbc..8f37a55e60 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/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)", diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 724e3a430c..73d615ffd3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/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)", diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 147fccda78..37cc7c7797 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/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)", diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 1febbb9bfd..cdab0805a1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/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, 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()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 316069280a..952756210c 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/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.setDeviceProfileId(deviceProfile.getId()); - return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.FAIL, user); + return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.DEFAULT, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java index 61d52f00bb..84d606251c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java +++ b/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 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); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index f55635d1d7..ef630c2df4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index 52b20fc52b..42fce5213e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/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); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index c070aea087..a2a0e6846d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/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; diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index 90b5cbeef2..bb0d90e45a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/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); diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index f312e49f71..08eecf3f10 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/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); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 36cede9e84..118e0f1137 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/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); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index 10b49dfe1d..0549e17e43 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/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-"); + } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index a930c86ac9..59c17fb4bd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/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 { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index d478e2099a..1cd5ff9ff1 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/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); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java new file mode 100644 index 0000000000..1685dbc933 --- /dev/null +++ b/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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java index b21ddce883..00b72f7223 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java +++ b/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); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java index 450b030e08..6c1ab74764 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java @@ -33,7 +33,9 @@ public interface Dao { ListenableFuture 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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 4b24e464bc..7735e718c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -243,5 +243,4 @@ public interface AssetDao extends Dao, TenantEntityDao, Exportable PageData findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink); - EntityInfo findEntityInfoByName(TenantId tenantId, String name); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 0414a0f4a7..7df76d9153 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/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, TenantEntityDao, Exporta PageData findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink); - EntityInfo findEntityInfoByName(TenantId tenantId, String name); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index adce656bef..6a14805ca7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/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 & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer setName, EntityType entityType) { + protected & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer 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); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index cd01473d69..9b344a8848 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/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 { 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()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index 75e7179391..b6827383b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/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 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; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 44d8a09ff4..bd4f39162f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/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