Introduce ResourceSubType for image resources.

This commit is contained in:
Igor Kulikov 2024-05-08 20:00:32 +03:00
parent 45d071538c
commit 7d0e5b330b
25 changed files with 214 additions and 12 deletions

View File

@ -0,0 +1,32 @@
--
-- Copyright © 2016-2024 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- UPDATE RESOURCE SUB TYPE START
DO
$$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'resource' AND column_name = 'resource_sub_type'
) THEN
ALTER TABLE resource ADD COLUMN resource_sub_type varchar(32);
UPDATE resource SET resource_sub_type = 'IMAGE' WHERE resource_type = 'IMAGE';
END IF;
END;
$$;
-- UPDATE RESOURCE SUB TYPE END

View File

@ -115,6 +115,8 @@ public class ControllerConstants {
protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
protected static final String RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION = "A string value representing resource sub-type.";
protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority.";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";

View File

@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
@ -64,6 +65,7 @@ import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
@ -95,7 +97,8 @@ public class ImageController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping("/api/image")
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
@RequestPart(required = false) String title) throws Exception {
@RequestPart(required = false) String title,
@RequestPart(required = false) String imageSubType) throws Exception {
SecurityUser user = getCurrentUser();
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
@ -108,7 +111,14 @@ public class ImageController extends BaseController {
} else {
image.setTitle(file.getOriginalFilename());
}
ResourceSubType subType = ResourceSubType.IMAGE;
if (StringUtils.isNotEmpty(imageSubType)) {
subType = ResourceSubType.valueOf(imageSubType);
}
image.setResourceType(ResourceType.IMAGE);
image.setResourceSubType(subType);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(file.getContentType());
image.setDescriptorValue(descriptor);
@ -193,6 +203,7 @@ public class ImageController extends BaseController {
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
.subType(imageInfo.getResourceSubType().name())
.resourceKey(imageInfo.getResourceKey())
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
@ -214,6 +225,11 @@ public class ImageController extends BaseController {
} else {
image.setTitle(imageData.getFileName());
}
if (StringUtils.isNotEmpty(imageData.getSubType())) {
image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType()));
} else {
image.setResourceSubType(ResourceSubType.IMAGE);
}
image.setResourceType(ResourceType.IMAGE);
image.setResourceKey(imageData.getResourceKey());
image.setPublic(imageData.isPublic());
@ -250,6 +266,8 @@ public class ImageController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"IMAGE", "IOT_SVG"}))
@RequestParam(required = false) String imageSubType,
@Parameter(description = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION)
@RequestParam(required = false) boolean includeSystemImages,
@Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@ -261,10 +279,14 @@ public class ImageController extends BaseController {
// PE: generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TenantId tenantId = getTenantId();
ResourceSubType subType = ResourceSubType.IMAGE;
if (StringUtils.isNotEmpty(imageSubType)) {
subType = ResourceSubType.valueOf(imageSubType);
}
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) {
return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink));
return checkNotNull(imageService.getImagesByTenantId(tenantId, subType, pageLink));
} else {
return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink));
return checkNotNull(imageService.getAllImagesByTenantId(tenantId, subType, pageLink));
}
}

View File

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

View File

@ -121,6 +121,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
case "3.6.4":
updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null);
break;
case "3.7.0":
updateSchema("3.7.0", 3007000, "3.7.1", 3007001, null);
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}

View File

@ -23,11 +23,14 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.page.PageData;
@ -35,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
@ -53,6 +57,7 @@ public class ImageControllerTest extends AbstractControllerTest {
@Before
public void beforeEach() throws Exception {
resourceRepository.deleteAll();
loginTenantAdmin();
}
@ -66,6 +71,8 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_png_image.png";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);
assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
assertThat(imageInfo.getTitle()).isEqualTo(filename);
assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE);
assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
@ -85,6 +92,8 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_jpeg_image.jpg";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);
assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
checkJpegImageDescriptor(imageDescriptor);
@ -97,6 +106,22 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_svg_image.svg";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE);
assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
checkSvgImageDescriptor(imageDescriptor);
assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE);
assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize());
}
@Test
public void testUploadIoTSvgImage() throws Exception {
String filename = "my_iot_svg_image.svg";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), filename, "image/svg+xml", SVG_IMAGE);
assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IOT_SVG);
ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
checkSvgImageDescriptor(imageDescriptor);
@ -175,6 +200,7 @@ public class ImageControllerTest extends AbstractControllerTest {
assertThat(exportData.getMediaType()).isEqualTo("image/png");
assertThat(exportData.getFileName()).isEqualTo(filename);
assertThat(exportData.getTitle()).isEqualTo(filename);
assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name());
assertThat(exportData.getResourceKey()).isEqualTo(filename);
assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE));
assertThat(exportData.isPublic()).isTrue();
@ -184,6 +210,7 @@ public class ImageControllerTest extends AbstractControllerTest {
TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class);
assertThat(importedImageInfo.getTitle()).isEqualTo(filename);
assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name());
assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename);
assertThat(importedImageInfo.getFileName()).isEqualTo(filename);
assertThat(importedImageInfo.isPublic()).isTrue();
@ -198,20 +225,37 @@ public class ImageControllerTest extends AbstractControllerTest {
String systemImageName = "my_system_png_image.png";
TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE);
String systemIotSvgName = "my_system_iot_svg_image.svg";
TbResourceInfo systemIotSvg = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), systemIotSvgName, "image/svg+xml", SVG_IMAGE);
loginTenantAdmin();
String tenantImageName = "my_jpeg_image.jpg";
TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE);
String tenantIotSvgName = "my_iot_svg_image.svg";
TbResourceInfo tenantIotSvg = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.IOT_SVG.name(), tenantIotSvgName, "image/svg+xml", SVG_IMAGE);
List<TbResourceInfo> tenantImages = getImages(null, false, 10);
assertThat(tenantImages).containsOnly(tenantImage);
List<TbResourceInfo> tenantIotSvgs = getImages(null, ResourceSubType.IOT_SVG.name(), false, 10);
assertThat(tenantIotSvgs).containsOnly(tenantIotSvg);
List<TbResourceInfo> allImages = getImages(null, true, 10);
assertThat(allImages).containsOnly(tenantImage, systemImage);
List<TbResourceInfo> allIotSvgs = getImages(null, ResourceSubType.IOT_SVG.name(), true, 10);
assertThat(allIotSvgs).containsOnly(tenantIotSvg, systemIotSvg);
assertThat(getImages("png", true, 10))
.containsOnly(systemImage);
assertThat(getImages("jpg", true, 10))
.containsOnly(tenantImage);
assertThat(getImages("my_system_iot", ResourceSubType.IOT_SVG.name(), true, 10))
.containsOnly(systemIotSvg);
assertThat(getImages("my_iot_svg", ResourceSubType.IOT_SVG.name(),true, 10))
.containsOnly(tenantIotSvg);
}
@Test
@ -312,7 +356,15 @@ public class ImageControllerTest extends AbstractControllerTest {
}
private List<TbResourceInfo> getImages(String searchText, boolean includeSystemImages, int limit) throws Exception {
PageData<TbResourceInfo> images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText));
return this.getImages(searchText, null, includeSystemImages, limit);
}
private List<TbResourceInfo> getImages(String searchText, String imageSubType, boolean includeSystemImages, int limit) throws Exception {
var url = "/api/images?includeSystemImages=" + includeSystemImages + "&";
if (StringUtils.isNotEmpty(imageSubType)) {
url += "imageSubType=" + imageSubType+ "&";
}
PageData<TbResourceInfo> images = doGetTypedWithPageLink(url, new TypeReference<>() {}, new PageLink(limit, 0, searchText));
return images.getData();
}
@ -332,8 +384,16 @@ public class ImageControllerTest extends AbstractControllerTest {
}
private <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception {
return this.uploadImage(httpMethod, url, null, filename, mediaType, content);
}
private <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception {
MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content);
var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file);
if (StringUtils.isNotEmpty(subType)) {
var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8));
request.part(imageSubTypePart);
}
setJwtToken(request);
return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);
}

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
@ -36,9 +37,9 @@ public interface ImageService {
TbResourceInfo getPublicImageInfoByKey(String publicResourceKey);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);
PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);
byte[] getImageData(TenantId tenantId, TbResourceId imageId);

View File

@ -33,6 +33,7 @@ public class ImageExportData {
private String mediaType;
private String fileName;
private String title;
private String subType;
private String resourceKey;
private boolean isPublic;
private String publicResourceKey;

View File

@ -0,0 +1,6 @@
package org.thingsboard.server.common.data;
public enum ResourceSubType {
IMAGE,
IOT_SVG
}

View File

@ -46,6 +46,8 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
private String title;
@Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY)
private ResourceType resourceType;
@Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY)
private ResourceSubType resourceSubType;
@NoXss
@Length(fieldName = "resourceKey")
@Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY)
@ -78,6 +80,7 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
this.tenantId = resourceInfo.tenantId;
this.title = resourceInfo.title;
this.resourceType = resourceInfo.resourceType;
this.resourceSubType = resourceInfo.resourceSubType;
this.resourceKey = resourceInfo.resourceKey;
this.searchText = resourceInfo.searchText;
this.isPublic = resourceInfo.isPublic;

View File

@ -27,5 +27,6 @@ public class TbResourceInfoFilter {
private TenantId tenantId;
private Set<ResourceType> resourceTypes;
private Set<ResourceSubType> resourceSubTypes;
}

View File

@ -27,6 +27,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.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant;
@ -827,6 +828,9 @@ public class ProtoUtils {
if (isNotNull(resource.getPreview())) {
builder.setPreview(ByteString.copyFrom(resource.getPreview()));
}
if (isNotNull(resource.getResourceSubType())) {
builder.setResourceSubType(resource.getResourceSubType().name());
}
return builder.build();
}
@ -858,6 +862,9 @@ public class ProtoUtils {
if (proto.hasPreview()) {
resource.setPreview(proto.getPreview().toByteArray());
}
if (proto.hasResourceSubType()) {
resource.setResourceSubType(ResourceSubType.valueOf(proto.getResourceSubType()));
}
return resource;
}

View File

@ -286,6 +286,7 @@ message TbResourceProto {
optional int64 externalIdLSB = 16;
optional bytes data = 17;
optional bytes preview = 18;
optional string resourceSubType = 19;
}
message ApiUsageStateProto {

View File

@ -498,6 +498,7 @@ public class ModelConstants {
public static final String RESOURCE_TABLE_NAME = "resource";
public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String RESOURCE_TYPE_COLUMN = "resource_type";
public static final String RESOURCE_SUB_TYPE_COLUMN = "resource_sub_type";
public static final String RESOURCE_KEY_COLUMN = "resource_key";
public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY;
public static final String RESOURCE_FILE_NAME_COLUMN = "file_name";

View File

@ -20,6 +20,7 @@ import jakarta.persistence.Convert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -42,6 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
@ -63,6 +65,9 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> {
@Column(name = RESOURCE_TYPE_COLUMN)
private String resourceType;
@Column(name = RESOURCE_SUB_TYPE_COLUMN)
private String resourceSubType;
@Column(name = RESOURCE_KEY_COLUMN)
private String resourceKey;
@ -107,6 +112,9 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> {
}
this.title = resource.getTitle();
this.resourceType = resource.getResourceType().name();
if (resource.getResourceSubType() != null) {
this.resourceSubType = resource.getResourceSubType().name();
}
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.fileName = resource.getFileName();
@ -126,6 +134,7 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> {
resource.setTenantId(TenantId.fromUUID(tenantId));
resource.setTitle(title);
resource.setResourceType(ResourceType.valueOf(resourceType));
resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null);
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setFileName(fileName);

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Convert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -39,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
@ -60,6 +62,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
@Column(name = RESOURCE_TYPE_COLUMN)
private String resourceType;
@Column(name = RESOURCE_SUB_TYPE_COLUMN)
private String resourceSubType;
@Column(name = RESOURCE_KEY_COLUMN)
private String resourceKey;
@ -96,6 +101,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
this.tenantId = resource.getTenantId().getId();
this.title = resource.getTitle();
this.resourceType = resource.getResourceType().name();
if (resource.getResourceSubType() != null) {
this.resourceSubType = resource.getResourceSubType().name();
}
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.etag = resource.getEtag();
@ -113,6 +121,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
resource.setTenantId(TenantId.fromUUID(tenantId));
resource.setTitle(title);
resource.setResourceType(ResourceType.valueOf(resourceType));
resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null);
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setEtag(etag);

View File

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
@ -140,6 +141,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
if (image.getId() == null) {
image.setResourceKey(getUniqueKey(image.getTenantId(), StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName())));
}
if (image.getResourceSubType() == null) {
image.setResourceSubType(ResourceSubType.IMAGE);
}
resourceValidator.validate(image, TbResourceInfo::getTenantId);
ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class);
@ -220,21 +224,23 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
@Override
public PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) {
log.trace("Executing getImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.resourceSubTypes(Set.of(imageSubType))
.build();
return findTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) {
log.trace("Executing getAllImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.resourceSubTypes(Set.of(imageSubType))
.build();
return findAllTenantResourcesByTenantId(filter, pageLink);
}

View File

@ -184,7 +184,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public List<TbResource> findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) {
log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null);
return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, objectIds, null);
}
@Override
@ -198,7 +198,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public PageData<TbResource> findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) {
log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink);
return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, pageLink);
}
@Override

View File

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -35,10 +36,12 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao,
PageData<TbResource> findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
ResourceSubType resourceSubType,
PageLink pageLink);
List<TbResource> findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
ResourceSubType resourceSubType,
String[] objectIds,
String searchText);

View File

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -68,11 +69,13 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
@Override
public PageData<TbResource> findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
ResourceSubType resourceSubType,
PageLink pageLink) {
return DaoUtil.toPageData(resourceRepository.findResourcesPage(
tenantId.getId(),
TenantId.SYS_TENANT_ID.getId(),
resourceType.name(),
resourceSubType != null ? resourceSubType.name() : null,
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)
));
@ -80,6 +83,7 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
@Override
public List<TbResource> findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType,
ResourceSubType resourceSubType,
String[] objectIds,
String searchText) {
return objectIds == null ?
@ -87,6 +91,7 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
tenantId.getId(),
TenantId.SYS_TENANT_ID.getId(),
resourceType.name(),
resourceSubType != null ? resourceSubType.name() : null,
searchText)) :
DaoUtil.convertDataList(resourceRepository.findResourcesByIds(
tenantId.getId(),

View File

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
@ -63,10 +64,13 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
if (CollectionsUtil.isEmpty(resourceTypes)) {
resourceTypes = EnumSet.allOf(ResourceType.class);
}
Set<ResourceSubType> resourceSubTypes = filter.getResourceSubTypes();
return DaoUtil.toPageData(resourceInfoRepository
.findAllTenantResourcesByTenantId(
filter.getTenantId().getId(), TenantId.NULL_UUID,
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
CollectionsUtil.isEmpty(resourceSubTypes) ? null :
resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@ -77,10 +81,13 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
if (CollectionsUtil.isEmpty(resourceTypes)) {
resourceTypes = EnumSet.allOf(ResourceType.class);
}
Set<ResourceSubType> resourceSubTypes = filter.getResourceSubTypes();
return DaoUtil.toPageData(resourceInfoRepository
.findTenantResourcesByTenantId(
filter.getTenantId().getId(),
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
CollectionsUtil.isEmpty(resourceSubTypes) ? null :
resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()),
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)));
}

View File

@ -37,19 +37,23 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
"WHERE sr.tenantId = :tenantId " +
"AND tr.resourceType = sr.resourceType " +
"AND tr.resourceKey = sr.resourceKey)))" +
"AND tr.resourceType IN :resourceTypes")
"AND tr.resourceType IN :resourceTypes " +
"AND (:resourceSubTypes IS NULL OR tr.resourceSubType IN :resourceSubTypes)")
Page<TbResourceInfoEntity> findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("systemTenantId") UUID systemTenantId,
@Param("resourceTypes") List<String> resourceTypes,
@Param("resourceSubTypes") List<String> resourceSubTypes,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
"ri.tenantId = :tenantId " +
"AND ri.resourceType IN :resourceTypes " +
"AND (:resourceSubTypes IS NULL OR ri.resourceSubType IN :resourceSubTypes) " +
"AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)")
Page<TbResourceInfoEntity> findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("resourceTypes") List<String> resourceTypes,
@Param("resourceSubTypes") List<String> resourceSubTypes,
@Param("searchText") String searchText,
Pageable pageable);

View File

@ -34,6 +34,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
@Query("SELECT tr FROM TbResourceEntity tr " +
"WHERE tr.resourceType = :resourceType " +
"AND (:resourceSubType IS NULL OR tr.resourceSubType = :resourceSubType) " +
"AND (:searchText IS NULL OR ilike(tr.searchText, CONCAT('%', :searchText, '%')) = true) " +
"AND (tr.tenantId = :tenantId " +
"OR (tr.tenantId = :systemAdminId " +
@ -46,11 +47,13 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysAdminId,
@Param("resourceType") String resourceType,
@Param("resourceSubType") String resourceSubType,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT tr FROM TbResourceEntity tr " +
"WHERE tr.resourceType = :resourceType " +
"AND (:resourceSubType IS NULL OR tr.resourceSubType = :resourceSubType) " +
"AND (:searchText IS NULL OR ilike(tr.searchText, CONCAT('%', :searchText, '%')) = true) " +
"AND (tr.tenantId = :tenantId " +
"OR (tr.tenantId = :systemAdminId " +
@ -62,6 +65,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
List<TbResourceEntity> findResources(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysAdminId,
@Param("resourceType") String resourceType,
@Param("resourceSubType") String resourceSubType,
@Param("searchText") String searchText);
@Query("SELECT tr FROM TbResourceEntity tr " +

View File

@ -715,6 +715,7 @@ CREATE TABLE IF NOT EXISTS resource (
tenant_id uuid NOT NULL,
title varchar(255) NOT NULL,
resource_type varchar(32) NOT NULL,
resource_sub_type varchar(32),
resource_key varchar(255) NOT NULL,
search_text varchar(255),
file_name varchar(255) NOT NULL,

View File

@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
@ -3603,10 +3604,19 @@ public class RestClient implements Closeable {
}
public PageData<TbResourceInfo> getImages(PageLink pageLink, boolean includeSystemImages) {
return this.getImages(pageLink, null, includeSystemImages);
}
public PageData<TbResourceInfo> getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) {
Map<String, String> params = new HashMap<>();
var url = baseURL + "/api/images?includeSystemImages={includeSystemImages}&";
addPageLinkToParam(params, pageLink);
params.put("includeSystemImages", String.valueOf(includeSystemImages));
return restTemplate.exchange(baseURL + "/api/images?includeSystemImages={includeSystemImages}&" + getUrlParams(pageLink),
if (imageSubType != null) {
url += "imageSubType={imageSubType}&";
params.put("imageSubType", imageSubType.name());
}
return restTemplate.exchange(url + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<TbResourceInfo>>() {},