diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 69c3181e26..9659be1941 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -35,7 +35,6 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.Optional; -import java.util.Set; import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION; @@ -48,9 +47,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO @RestController @RequestMapping("/api/ai-model-settings") -public class AiModelSettingsController extends BaseController { - - private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name"); +class AiModelSettingsController extends BaseController { @ApiOperation( value = "Create or update AI model settings (saveAiModelSettings)", @@ -75,16 +72,16 @@ public class AiModelSettingsController extends BaseController { TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/{aiModelSettingsId}") + @GetMapping("/{settingsUuid}") public AiModelSettings getAiModelSettingsById( @Parameter( description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid + @PathVariable UUID settingsUuid ) throws ThingsboardException { - return checkAiModelSettingsId(new AiModelSettingsId(aiModelSettingsUuid), Operation.READ); + return checkAiModelSettingsId(new AiModelSettingsId(settingsUuid), Operation.READ); } @ApiOperation( @@ -101,24 +98,17 @@ public class AiModelSettingsController extends BaseController { @RequestParam int page, @Parameter(description = AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder ) throws ThingsboardException { var user = getCurrentUser(); accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.READ); - validateSortProperty(sortProperty); var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink); } - private static void validateSortProperty(String sortProperty) { - if (sortProperty != null && !ALLOWED_SORT_PROPERTIES.contains(sortProperty)) { - throw new IllegalArgumentException("Unsupported sort property '" + sortProperty + "'! Only '" + String.join("', '", ALLOWED_SORT_PROPERTIES) + "' are allowed."); - } - } - @ApiOperation( value = "Delete AI model settings by ID (deleteAiModelSettingsById)", notes = "Deletes the AI model settings record by its `id`. " + @@ -127,17 +117,17 @@ public class AiModelSettingsController extends BaseController { TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/{aiModelSettingsId}") + @DeleteMapping("/{settingsUuid}") public boolean deleteAiModelSettingsById( @Parameter( description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid + @PathVariable UUID settingsUuid ) throws ThingsboardException { var user = getCurrentUser(); - var settingsId = new AiModelSettingsId(aiModelSettingsUuid); + var settingsId = new AiModelSettingsId(settingsUuid); accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE); Optional toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId); if (toDelete.isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 43b651bb24..7b158fe9a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -43,10 +43,9 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; -public abstract class DaoUtil { +public final class DaoUtil { - private DaoUtil() { - } + private DaoUtil() {} public static PageData toPageData(Page> page) { List data = convertDataList(page.getContent()); @@ -98,17 +97,17 @@ public abstract class DaoUtil { return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(sortOrders, columnMap, addDefaultSorting)); } - public static List convertDataList(Collection> toDataList) { - List list = Collections.emptyList(); - if (toDataList != null && !toDataList.isEmpty()) { - list = new ArrayList<>(); - for (ToData object : toDataList) { - if (object != null) { - list.add(object.toData()); - } + public static List convertDataList(Collection> toConvert) { + if (CollectionUtils.isEmpty(toConvert)) { + return Collections.emptyList(); + } + List converted = new ArrayList<>(toConvert.size()); + for (ToData object : toConvert) { + if (object != null) { + converted.add(object.toData()); } } - return list; + return converted; } public static T getData(ToData data) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 3875169305..411a67ca98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; @@ -92,7 +93,7 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { - validatePageLink(pageLink); + validatePageLink(pageLink, AiModelSettingsEntity.ALLOWED_SORT_PROPERTIES); return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java index 7d27a44cd1..2a866c8620 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java @@ -31,8 +31,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; @Getter @@ -43,7 +47,13 @@ import java.util.UUID; public class AiModelSettingsEntity extends BaseVersionedEntity { public static final Map COLUMN_MAP = Map.of( - "createdTime", "created_time" + "createdTime", "created_time", + "provider", "(configuration -> 'providerConfig' ->> 'provider')", + "modelId", "(configuration -> 'modelConfig' ->> 'modelId')" + ); + + public static final Set ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( + new LinkedHashSet<>(List.of("createdTime", "name", "provider", "modelId")) ); @Column(name = ModelConstants.AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index e051e99dc5..f94026ce89 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -26,11 +26,14 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.regex.Pattern; -public class Validator { +public final class Validator { + + private Validator() {} public static final Pattern PROPERTY_PATTERN = Pattern.compile("^[\\p{L}0-9_-]+$"); // Unicode letters, numbers, '_' and '-' allowed @@ -204,22 +207,61 @@ public class Validator { } /** - * This method validate PageLink page link. If pageLink is invalid than throw - * IncorrectParameterException exception + * Validates the specified PageLink object delegating to {@link #validatePageLink(PageLink, Set)} + * with no restrictions on allowed sort properties. * - * @param pageLink the page link + * @param pageLink the PageLink object to validate + * @throws IncorrectParameterException if the pageLink is null, has invalid page size, + * invalid page number, or invalid sort property + * @see #validatePageLink(PageLink, Set) */ public static void validatePageLink(PageLink pageLink) { + validatePageLink(pageLink, null); + } + + /** + * Validates the specified PageLink object ensuring that: + *
    + *
  • The PageLink object is not null
  • + *
  • The page size is greater than zero
  • + *
  • The page number is non-negative
  • + *
  • If sorting is specified, the sort property is valid and allowed
  • + *
+ * + *

When {@code allowedSortProperties} is provided, the sort property + * must be contained within this set. If {@code allowedSortProperties} is null, + * only basic sort property validation is performed. + * + * @param pageLink the PageLink object to validate. + * @param allowedSortProperties a Set of allowed sort property names, or null to skip + * this validation. If provided and the PageLink contains + * a sort order, the sort property must be in this set. + * @throws IncorrectParameterException if any of the following conditions are met: + *

    + *
  • {@code pageLink} is null
  • + *
  • page size is less than 1
  • + *
  • page number is negative
  • + *
  • sort property is malformed
  • + *
  • sort property is not in the {@code allowedSortProperties} set (when the set is provided and not null)
  • + *
+ */ + public static void validatePageLink(PageLink pageLink, Set allowedSortProperties) { if (pageLink == null) { throw new IncorrectParameterException("Page link must be specified."); } else if (pageLink.getPageSize() < 1) { - throw new IncorrectParameterException("Incorrect page link page size '"+pageLink.getPageSize()+"'. Page size must be greater than zero."); + throw new IncorrectParameterException("Incorrect page link page size '" + pageLink.getPageSize() + "'. Page size must be greater than zero."); } else if (pageLink.getPage() < 0) { - throw new IncorrectParameterException("Incorrect page link page '"+pageLink.getPage()+"'. Page must be positive integer."); + throw new IncorrectParameterException("Incorrect page link page '" + pageLink.getPage() + "'. Page must be positive integer."); } else if (pageLink.getSortOrder() != null) { - if (!isValidProperty(pageLink.getSortOrder().getProperty())) { + String sortProperty = pageLink.getSortOrder().getProperty(); + if (!isValidProperty(sortProperty)) { throw new IncorrectParameterException("Invalid page link sort property"); } + if (allowedSortProperties != null && !allowedSortProperties.contains(sortProperty)) { + throw new IncorrectParameterException( + "Unsupported sort property '" + sortProperty + "'. Only '" + String.join("', '", allowedSortProperties) + "' are allowed." + ); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java index e7b87b0a1a..a09179d31e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java @@ -17,6 +17,9 @@ package org.thingsboard.server.dao.sql.ai; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; @@ -25,6 +28,7 @@ import org.thingsboard.server.common.data.id.AiModelSettingsId; 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.common.data.page.SortOrder; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.ai.AiModelSettingsDao; import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; @@ -65,15 +69,27 @@ class JpaAiModelSettingsDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( - tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) + tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) ); } @Override public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.pageToPageData( - aiModelSettingsRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)).map(AiModelSettingsId::new) - ); + return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new)); + } + + private static PageRequest toPageRequest(PageLink pageLink) { + Sort sort; + SortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder == null) { + sort = Sort.by(Sort.Direction.ASC, "id"); + } else { + sort = JpaSort.unsafe( + Sort.Direction.fromString(sortOrder.getDirection().name()), + AiModelSettingsEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty()) + ); + } + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); } @Override