Add gzip compression for load dashboard and download svg images methods.
This commit is contained in:
		
							parent
							
								
									ab2e788057
								
							
						
					
					
						commit
						749df3f212
					
				@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import jakarta.mail.MessagingException;
 | 
			
		||||
import jakarta.servlet.ServletOutputStream;
 | 
			
		||||
import jakarta.servlet.http.HttpServletResponse;
 | 
			
		||||
import jakarta.validation.ConstraintViolation;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
@ -28,6 +29,7 @@ import org.slf4j.Logger;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.dao.DataAccessException;
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.http.ResponseEntity;
 | 
			
		||||
@ -182,7 +184,9 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@ -194,6 +198,7 @@ import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import java.util.zip.GZIPOutputStream;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
 | 
			
		||||
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
 | 
			
		||||
@ -1008,6 +1013,22 @@ public abstract class BaseController {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void compressResponseWithGzipIFAccepted(String acceptEncodingHeader, HttpServletResponse response, byte[] content) throws IOException {
 | 
			
		||||
        if (StringUtils.isNotEmpty(acceptEncodingHeader) && acceptEncodingHeader.contains("gzip")) {
 | 
			
		||||
            response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
 | 
			
		||||
            response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
 | 
			
		||||
            try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(response.getOutputStream())) {
 | 
			
		||||
                gzipOutputStream.write(content);
 | 
			
		||||
                gzipOutputStream.finish();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            try (ServletOutputStream outputStream = response.getOutputStream()) {
 | 
			
		||||
                outputStream.write(content);
 | 
			
		||||
                outputStream.flush();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected <T> ResponseEntity<T> response(HttpStatus status) {
 | 
			
		||||
        return ResponseEntity.status(status).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -22,8 +22,10 @@ import io.swagger.v3.oas.annotations.media.Content;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
 | 
			
		||||
import jakarta.servlet.http.HttpServletResponse;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.security.access.prepost.PreAuthorize;
 | 
			
		||||
import org.springframework.web.bind.annotation.DeleteMapping;
 | 
			
		||||
@ -31,6 +33,7 @@ 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.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;
 | 
			
		||||
@ -67,6 +70,7 @@ import java.util.Set;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION;
 | 
			
		||||
@ -149,17 +153,20 @@ public class DashboardController extends BaseController {
 | 
			
		||||
    )
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @GetMapping(value = "/dashboard/{dashboardId}")
 | 
			
		||||
    public Dashboard getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
 | 
			
		||||
    public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
 | 
			
		||||
                                      @PathVariable(DASHBOARD_ID) String strDashboardId,
 | 
			
		||||
                                      @Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
 | 
			
		||||
                                      @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources) throws ThingsboardException {
 | 
			
		||||
                                      @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources,
 | 
			
		||||
                                      @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
 | 
			
		||||
                                      HttpServletResponse response) throws Exception {
 | 
			
		||||
        checkParameter(DASHBOARD_ID, strDashboardId);
 | 
			
		||||
        DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
 | 
			
		||||
        Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
 | 
			
		||||
        if (includeResources) {
 | 
			
		||||
            dashboard.setResources(tbResourceService.exportResources(dashboard, getCurrentUser()));
 | 
			
		||||
        }
 | 
			
		||||
        return dashboard;
 | 
			
		||||
        response.setContentType(APPLICATION_JSON_VALUE);
 | 
			
		||||
        compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(dashboard));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)",
 | 
			
		||||
@ -171,11 +178,15 @@ public class DashboardController extends BaseController {
 | 
			
		||||
                    TENANT_AUTHORITY_PARAGRAPH)
 | 
			
		||||
    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
 | 
			
		||||
    @PostMapping(value = "/dashboard")
 | 
			
		||||
    public Dashboard saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
 | 
			
		||||
                                   @RequestBody Dashboard dashboard) throws Exception {
 | 
			
		||||
    public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
 | 
			
		||||
                                   @RequestBody Dashboard dashboard,
 | 
			
		||||
                                   @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
 | 
			
		||||
                                   HttpServletResponse response) throws Exception {
 | 
			
		||||
        dashboard.setTenantId(getTenantId());
 | 
			
		||||
        checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
 | 
			
		||||
        return tbDashboardService.save(dashboard, getCurrentUser());
 | 
			
		||||
        var savedDashboard = tbDashboardService.save(dashboard, getCurrentUser());
 | 
			
		||||
        response.setContentType(APPLICATION_JSON_VALUE);
 | 
			
		||||
        compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(savedDashboard));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Delete the Dashboard (deleteDashboard)",
 | 
			
		||||
@ -408,12 +419,13 @@ public class DashboardController extends BaseController {
 | 
			
		||||
                    "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. "
 | 
			
		||||
                    + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public HomeDashboard getHomeDashboard() throws ThingsboardException {
 | 
			
		||||
    @GetMapping(value = "/dashboard/home")
 | 
			
		||||
    public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
 | 
			
		||||
                                 HttpServletResponse response) throws Exception {
 | 
			
		||||
        SecurityUser securityUser = getCurrentUser();
 | 
			
		||||
        response.setContentType(APPLICATION_JSON_VALUE);
 | 
			
		||||
        if (securityUser.isSystemAdmin()) {
 | 
			
		||||
            return null;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
 | 
			
		||||
        JsonNode additionalInfo = user.getAdditionalInfo();
 | 
			
		||||
@ -431,7 +443,9 @@ public class DashboardController extends BaseController {
 | 
			
		||||
                homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return homeDashboard;
 | 
			
		||||
        if (homeDashboard != null) {
 | 
			
		||||
            compressResponseWithGzipIFAccepted(acceptEncodingHeader, response, JacksonUtil.writeValueAsBytes(homeDashboard));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get Home Dashboard Info (getHomeDashboardInfo)",
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,9 @@ import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.permission.Operation;
 | 
			
		||||
import org.thingsboard.server.service.security.permission.Resource;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.zip.GZIPOutputStream;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
 | 
			
		||||
@ -70,6 +72,7 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INC
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
 | 
			
		||||
import static org.thingsboard.server.dao.util.ImageUtils.mediaTypeToFileExtension;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@RestController
 | 
			
		||||
@ -179,15 +182,17 @@ public class ImageController extends BaseController {
 | 
			
		||||
                                                           @PathVariable String type,
 | 
			
		||||
                                                           @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)
 | 
			
		||||
                                                           @PathVariable String key,
 | 
			
		||||
                                                           @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
 | 
			
		||||
        return downloadIfChanged(type, key, etag, false);
 | 
			
		||||
                                                           @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag,
 | 
			
		||||
                                                           @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception {
 | 
			
		||||
        return downloadIfChanged(type, key, etag, acceptEncodingHeader, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping(value = "/api/images/public/{publicResourceKey}", produces = "image/*")
 | 
			
		||||
    public ResponseEntity<ByteArrayResource> downloadPublicImage(@PathVariable String publicResourceKey,
 | 
			
		||||
                                                                 @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
 | 
			
		||||
                                                                 @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag,
 | 
			
		||||
                                                                 @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception {
 | 
			
		||||
        ImageCacheKey cacheKey = ImageCacheKey.forPublicImage(publicResourceKey);
 | 
			
		||||
        return downloadIfChanged(cacheKey, etag, () -> imageService.getPublicImageInfoByKey(publicResourceKey));
 | 
			
		||||
        return downloadIfChanged(cacheKey, etag, acceptEncodingHeader, () -> imageService.getPublicImageInfoByKey(publicResourceKey));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
 | 
			
		||||
@ -213,8 +218,9 @@ public class ImageController extends BaseController {
 | 
			
		||||
                                                                  @PathVariable String type,
 | 
			
		||||
                                                                  @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)
 | 
			
		||||
                                                                  @PathVariable String key,
 | 
			
		||||
                                                                  @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
 | 
			
		||||
        return downloadIfChanged(type, key, etag, true);
 | 
			
		||||
                                                                  @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag,
 | 
			
		||||
                                                                  @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader) throws Exception {
 | 
			
		||||
        return downloadIfChanged(type, key, etag, acceptEncodingHeader, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
 | 
			
		||||
@ -268,12 +274,12 @@ public class ImageController extends BaseController {
 | 
			
		||||
        return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws Exception {
 | 
			
		||||
    private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, String acceptEncodingHeader, boolean preview) throws Exception {
 | 
			
		||||
        ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview);
 | 
			
		||||
        return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ));
 | 
			
		||||
        return downloadIfChanged(cacheKey, etag, acceptEncodingHeader, () -> checkImageInfo(type, key, Operation.READ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ResponseEntity<ByteArrayResource> downloadIfChanged(ImageCacheKey cacheKey, String etag, ThrowingSupplier<TbResourceInfo> imageInfoSupplier) throws Exception {
 | 
			
		||||
    private ResponseEntity<ByteArrayResource> downloadIfChanged(ImageCacheKey cacheKey, String etag, String acceptEncodingHeader, ThrowingSupplier<TbResourceInfo> imageInfoSupplier) throws Exception {
 | 
			
		||||
        if (StringUtils.isNotEmpty(etag)) {
 | 
			
		||||
            etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
 | 
			
		||||
            if (etag.equals(tbImageService.getETag(cacheKey))) {
 | 
			
		||||
@ -294,7 +300,6 @@ public class ImageController extends BaseController {
 | 
			
		||||
        tbImageService.putETag(cacheKey, descriptor.getEtag());
 | 
			
		||||
        var result = ResponseEntity.ok()
 | 
			
		||||
                .header("Content-Type", descriptor.getMediaType())
 | 
			
		||||
                .contentLength(data.length)
 | 
			
		||||
                .eTag(descriptor.getEtag());
 | 
			
		||||
        if (!cacheKey.isPublic()) {
 | 
			
		||||
            result
 | 
			
		||||
@ -308,7 +313,19 @@ public class ImageController extends BaseController {
 | 
			
		||||
        } else {
 | 
			
		||||
            result.cacheControl(CacheControl.noCache());
 | 
			
		||||
        }
 | 
			
		||||
        return result.body(new ByteArrayResource(data));
 | 
			
		||||
        var responseData = data;
 | 
			
		||||
        if (mediaTypeToFileExtension(descriptor.getMediaType()).equals("svg") &&
 | 
			
		||||
                StringUtils.isNotEmpty(acceptEncodingHeader) && acceptEncodingHeader.contains("gzip")) {
 | 
			
		||||
            result.header(HttpHeaders.CONTENT_ENCODING, "gzip");
 | 
			
		||||
            var outputStream = new ByteArrayOutputStream();
 | 
			
		||||
            try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
 | 
			
		||||
                gzipOutputStream.write(data);
 | 
			
		||||
                gzipOutputStream.finish();
 | 
			
		||||
            }
 | 
			
		||||
            responseData = outputStream.toByteArray();
 | 
			
		||||
        }
 | 
			
		||||
        result.contentLength(responseData.length);
 | 
			
		||||
        return result.body(new ByteArrayResource(responseData));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private TbResourceInfo checkImageInfo(String imageType, String key, Operation operation) throws ThingsboardException {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user