Browse Source

Upload resource using multipart endpoint

pull/14205/head
Andrii Landiak 10 months ago
parent
commit
57527f2c97
  1. 67
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  4. 71
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  5. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  6. 7
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  7. 9
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

67
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -32,12 +33,16 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
@ -67,6 +72,7 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
@ -215,6 +221,7 @@ public class TbResourceController extends BaseController {
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Resource entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Deprecated // resource should be save or update with an upload request
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource")
public TbResourceInfo saveResource(@Parameter(description = "A JSON value representing the Resource.")
@ -224,6 +231,66 @@ public class TbResourceController extends BaseController {
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Upload Resource via Multipart File (uploadResource)",
notes = "Upload the Resource using multipart file upload. " +
"When creating the Resource, platform generates Resource id as " + UUID_WIKI_LINK +
"The newly created Resource id will be present in the response. " +
"Specify existing Resource id to update the Resource. " +
"Referencing non-existing Resource Id will cause 'Not Found' error. " +
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@RequestParam(name = RESOURCE_ID, required = false) UUID resourceId,
@Parameter(description = "Resource title.", example = "Title")
@RequestParam(name = "title", required = false) String title,
@Parameter(description = "Resource type.", schema = @Schema(implementation = ResourceType.class, nullable = true, example = "GENERAL"))
@RequestParam(name = "resourceType") ResourceType resourceType,
@Parameter(description = "Resource descriptor (JSON).")
@RequestParam(name = "descriptor", required = false) String descriptor,
@Parameter(description = "Resource search text.")
@RequestParam(name = "searchText", required = false) String searchText,
@Parameter(description = "Resource file.")
@RequestPart MultipartFile file) throws Exception {
TbResource resource = new TbResource();
resource.setTenantId(getTenantId());
resource.setId(resourceId != null ? new TbResourceId(resourceId) : null);
resource.setTitle(StringUtils.isNotEmpty(title) ? title : file.getOriginalFilename());
resource.setResourceType(resourceType);
if (StringUtils.isNotEmpty(descriptor)) {
resource.setDescriptor(JacksonUtil.toJsonNode(descriptor));
} else {
String mediaType = resourceType.getMediaType() != null ? resourceType.getMediaType() : file.getContentType();
resource.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType));
}
resource.setSearchText(StringUtils.isNotEmpty(searchText) ? searchText : resource.getTitle());
resource.setFileName(file.getOriginalFilename());
resource.setData(file.getBytes());
checkEntity(resource.getId(), resource, Resource.TB_RESOURCE);
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Update Resource title",
notes = "Updates the title of the existing Resource by resourceId. " +
"Only the title can be updated. " +
"Referencing a non-existing Resource Id will cause a 'Not Found' error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/resource/{id}/title")
public TbResourceInfo updateResourceTitle(@Parameter(description = "Unique identifier of the Resource to update", required = true)
@PathVariable UUID id,
@Parameter(description = "New title for the Resource", example = "Title", required = true)
@RequestBody String title) throws Exception {
TbResourceId tbResourceId = new TbResourceId(id);
TbResource resourceInfo = new TbResource(checkResourceInfoId(tbResourceId, Operation.WRITE));
resourceInfo.setTitle(title);
return tbResourceService.save(resourceInfo, getCurrentUser());
}
@ApiOperation(value = "Get Resource Infos (getResources)",
notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)

2
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -75,7 +75,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = resource.getTenantId();
try {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) && resource.getId() == null) {
toLwm2mResource(resource);
} else if (resource.getResourceKey() == null) {
resource.setResourceKey(resource.getFileName());

2
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;

71
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -25,11 +25,12 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
@ -47,15 +48,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -64,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class TbResourceControllerTest extends AbstractControllerTest {
private IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private final IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private static final String DEFAULT_FILE_NAME = "test.jks";
private static final String DEFAULT_FILE_NAME_2 = "test2.jks";
@ -126,13 +126,9 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey());
Assert.assertArrayEquals(resource.getData(), download(savedResource.getId()));
TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class);
foundResource.setTitle("My new resource");
foundResource.setData(null);
savedResource = save(foundResource);
Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle());
String resourceTitle = "My new resource";
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/title", resourceTitle, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
@ -501,8 +497,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED, cntEntity, cntEntity, cntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -549,8 +545,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED,
jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -581,8 +577,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
@ -654,8 +650,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(jksResources, idComparator);
Collections.sort(loadedResources, idComparator);
jksResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(jksResources, loadedResources);
@ -736,8 +732,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(expectedResources, idComparator);
Collections.sort(loadedResources, idComparator);
expectedResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(expectedResources, loadedResources);
@ -770,7 +766,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -814,7 +810,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -859,10 +855,9 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("can't be updated")));
foundResource.setData(null);
foundResource.setTitle("Updated resource");
savedResource = doPost("/api/resource", foundResource, TbResource.class);
assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
String resourceTitle = "Updated resource";
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/title", resourceTitle, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
@ -923,8 +918,24 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
private TbResourceInfo save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
});
byte[] data = tbResource.getData() != null ? tbResource.getData() : tbResource.getEncodedData() != null ? Base64.getDecoder().decode(tbResource.getEncodedData()) : null;
List<MockPart> parts = new ArrayList<>();
parts.add(new MockPart("resourceType", tbResource.getResourceType().name().getBytes()));
if (tbResource.getId() != null) {
parts.add(new MockPart("resourceId", tbResource.getId().getId().toString().getBytes()));
}
if (tbResource.getTitle() != null) {
parts.add(new MockPart("title", tbResource.getTitle().getBytes()));
}
if (tbResource.getDescriptor() != null) {
parts.add(new MockPart("descriptor", tbResource.getDescriptor().toString().getBytes()));
}
if (tbResource.getSearchText() != null) {
parts.add(new MockPart("searchText", tbResource.getSearchText().getBytes()));
}
return uploadResource(HttpMethod.POST, "/api/resource/upload", tbResource.getFileName(), tbResource.getResourceType().getMediaType(), data, parts);
}
private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception {
@ -949,7 +960,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
for (String model : models) {
String fileName = model + ".xml";
byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName));
byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName)));
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.LWM2M_MODEL);

2
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.function.UnaryOperator;
@Schema
@ -36,6 +37,7 @@ import java.util.function.UnaryOperator;
@EqualsAndHashCode(callSuper = true)
public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, HasTenantId, ExportableEntity<TbResourceId> {
@Serial
private static final long serialVersionUID = 7282664529021651736L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)

7
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -89,7 +89,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Primary
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final int MAX_ENTITIES_TO_FIND = 10;
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@ -98,7 +100,6 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final RuleChainDao ruleChainDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap = new HashMap<>();
private final Map<EntityType, ResourceContainerDao<?>> generalResourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct
public void init() {
@ -275,7 +276,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public TbResource toResource(TenantId tenantId, ResourceExportData exportData) {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
throw new IllegalArgumentException("Image import not supported");
}

9
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -17,9 +17,11 @@ package org.thingsboard.server.dao.service.validator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
@ -54,8 +56,8 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Override
protected TbResource validateUpdate(TenantId tenantId, TbResource resource) {
if (resource.getData() != null && !resource.getResourceType().isUpdatable() &&
tenantId != null && !tenantId.isSysTenantId()) {
if ((resource.getData() != null && !resource.getResourceType().isUpdatable() && tenantId != null && !tenantId.isSysTenantId())
|| resource.getResourceType().equals(ResourceType.LWM2M_MODEL)) {
throw new DataValidationException("This type of resource can't be updated");
}
return resource;
@ -81,7 +83,7 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
if (StringUtils.isEmpty(resource.getFileName())) {
throw new DataValidationException("Resource file name should be specified!");
}
if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) {
if (Strings.CS.containsAny(resource.getFileName(), "/", "\\")) {
throw new DataValidationException("File name contains forbidden symbols");
}
if (StringUtils.isEmpty(resource.getResourceKey())) {
@ -104,4 +106,5 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
}
}

Loading…
Cancel
Save