Skip to content

Commit

Permalink
Feature/add new endpoint to resolve all images for a specific channel (
Browse files Browse the repository at this point in the history
  • Loading branch information
saschadoemer authored Jun 28, 2024
1 parent ae722d0 commit 86bf534
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.app.fivegla.controller.dto.request;

import de.app.fivegla.persistence.entity.enums.ImageChannel;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

/**
* Request to get all images for a transaction.
*/
@Getter
@Setter
@Schema(description = "Request to get all images for a transaction.")
public class GetAllImagesForTransactionRequest {

/**
* The transaction id.
*/
@NotBlank
@Schema(description = "The transaction id.")
private String transactionId;

/**
* The channel of the image.
*/
@NotNull
@Schema(description = "The channel of the image.")
private ImageChannel channel;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package de.app.fivegla.controller.dto.response;

import de.app.fivegla.api.Response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.time.Instant;
import java.util.Map;

/**
* Response class for retrieving all transactions for a tenant.
* Extends the base Response class.
*/
@Getter
@Setter
@Builder
@Schema(description = "Response to get all transactions for a tenant.")
public class AllTransactionsForTenantResponse extends Response {

/**
* The start of the search interval.
*/
@Schema(description = "The start of the search interval.")
private Instant from;

/**
* The end of the search interval.
*/
@Schema(description = "The end of the search interval.")
private Instant to;

/**
* The transaction id with the first image timestamp.
*/
@Schema(description = "The transaction id with the first image timestamp.")
private Map<String, Instant> transactionIdWithTheFirstImageTimestamp;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.app.fivegla.controller.dto.response;

import de.app.fivegla.api.Response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

/**
* Response to get all images for a transaction.
*/
@Getter
@Setter
@Builder
@Schema(description = "Response to get all images for a transaction.")
public class GetAllImagesForTransactionResponse extends Response {

/**
* The images as base64 encoded data.
*/
@Schema(description = "The images as base64 encoded data.")
private List<String> imagesAsBase64EncodedData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import de.app.fivegla.business.TenantService;
import de.app.fivegla.config.security.marker.TenantCredentialApiAccess;
import de.app.fivegla.controller.api.BaseMappings;
import de.app.fivegla.controller.dto.request.GetAllImagesForTransactionRequest;
import de.app.fivegla.controller.dto.request.ImageProcessingRequest;
import de.app.fivegla.controller.dto.response.AllTransactionsForTenantResponse;
import de.app.fivegla.controller.dto.response.GetAllImagesForTransactionResponse;
import de.app.fivegla.controller.dto.response.ImageProcessingResponse;
import de.app.fivegla.controller.dto.response.OidsForTransactionResponse;
import de.app.fivegla.integration.imageprocessing.ImageProcessingIntegrationService;
import de.app.fivegla.persistence.entity.Image;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -17,12 +21,17 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;

/**
Expand All @@ -32,12 +41,15 @@
@RestController
@RequiredArgsConstructor
@RequestMapping(BaseMappings.IMAGE_PROCESSING + "/images")
public class ImageProcessingController implements TenantCredentialApiAccess {
public class ImagesController implements TenantCredentialApiAccess {

private final ImageProcessingIntegrationService imageProcessingIntegrationService;
private final TenantService tenantService;
private final GroupService groupService;

@Value("${app.defaultSearchIntervalInDays}")
private long defaultSearchIntervalInDays;

/**
* Processes one or multiple images from the mica sense camera.
*
Expand Down Expand Up @@ -78,6 +90,58 @@ public ResponseEntity<? extends Response> processImage(@Valid @RequestBody @Para
.build());
}

@GetMapping(value = "/transaction", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<? extends Response> listAllTransactionsForTenant(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Parameter(description = "Start of the search interval.") Instant from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @Parameter(description = "End of the search interval.") Instant to, Principal principal) {
var tenant = validateTenant(tenantService, principal);
if (to == null) {
to = Instant.now().minus(defaultSearchIntervalInDays, ChronoUnit.DAYS);
}
var allTransactionsWithinTheTimeRange = imageProcessingIntegrationService.listAllTransactionsForTenant(from, to, tenant.getTenantId());
var mappedValues = new HashMap<String, Instant>();
allTransactionsWithinTheTimeRange.forEach(t -> mappedValues.put(t.transactionId(), t.timestampOfTheFirstImage()));
var response = AllTransactionsForTenantResponse.builder()
.from(from)
.to(to)
.transactionIdWithTheFirstImageTimestamp(mappedValues)
.build();
return ResponseEntity.ok(response);

}

/**
* Return all images for a transaction.
*/
@Operation(
operationId = "images.get-all-images-for-transaction",
description = "Returns all images for a transaction.",
tags = BaseMappings.IMAGE_PROCESSING
)
@ApiResponse(
responseCode = "200",
description = "The images were found and returned.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = byte[].class)
)
)
@ApiResponse(
responseCode = "400",
description = "The request is invalid.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Response.class)
)
)
@PostMapping(value = "/images-for-transaction", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<? extends Response> getAllImagesForTransaction(@Valid @RequestBody @Parameter(description = "The request to search for images.", required = true) GetAllImagesForTransactionRequest request, Principal principal) {
var tenant = validateTenant(tenantService, principal);
var images = imageProcessingIntegrationService.getAllImagesForTransaction(request.getTransactionId(), request.getChannel(), tenant.getTenantId());
var imagesAsBase64EncodedData = images.stream().map(Image::getBase64encodedImage).toList();
return ResponseEntity.ok(GetAllImagesForTransactionResponse.builder()
.imagesAsBase64EncodedData(imagesAsBase64EncodedData)
.build());
}

/**
* Return image as stream.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.app.fivegla.integration.imageprocessing;

import de.app.fivegla.api.dto.SortableImageOids;
import de.app.fivegla.integration.imageprocessing.dto.TransactionIdWithTheFirstImageTimestamp;
import de.app.fivegla.persistence.ImageRepository;
import de.app.fivegla.persistence.entity.Group;
import de.app.fivegla.persistence.entity.Image;
Expand All @@ -10,6 +11,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.*;

/**
Expand Down Expand Up @@ -79,4 +81,35 @@ public List<SortableImageOids> getImageOidsForTransaction(String transactionId)
.toList();
}

/**
* Retrieves all images for a given transaction.
*
* @param transactionId the ID of the transaction
* @param channel the channel of the image
* @param tenantId the ID of the tenant
* @return a list of images associated with the transaction
*/
public List<Image> getAllImagesForTransaction(String transactionId, ImageChannel channel, String tenantId) {
return imageRepository.findByTransactionIdAndChannelAndTenantTenantId(transactionId, channel, tenantId);
}

/**
* Retrieves a list of TransactionIdWithTheFirstImageTimestamp objects for a given tenant
* within a specified time frame.
*
* @param from the starting time instant
* @param to the ending time instant
* @param tenantId the unique identifier for the tenant
* @return a list of TransactionIdWithTheFirstImageTimestamp objects
*/
public List<TransactionIdWithTheFirstImageTimestamp> listAllTransactionsForTenant(Instant from, Instant to, String tenantId) {
var allTransactionsForTenant = new ArrayList<TransactionIdWithTheFirstImageTimestamp>();
var allTransactionIdsWithinTimeFrame = imageRepository.findAllTransactionIdsWithinTimeFrame(tenantId, from, to);
for (String transactionId : allTransactionIdsWithinTimeFrame) {
var image = imageRepository.findFirstByTransactionIdOrderByMeasuredAtAsc(transactionId);
var transactionIdWithTheFirstImageTimestamp = new TransactionIdWithTheFirstImageTimestamp(transactionId, image.getMeasuredAt().toInstant());
allTransactionsForTenant.add(transactionIdWithTheFirstImageTimestamp);
}
return allTransactionsForTenant;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.app.fivegla.integration.imageprocessing.dto;


import java.time.Instant;

/**
* The transaction id with the timestamp of the first image.
*
* @param transactionId The transaction id.
* @param timestampOfTheFirstImage The timestamp of the first image.
*/
public record TransactionIdWithTheFirstImageTimestamp(String transactionId, Instant timestampOfTheFirstImage) {
}
34 changes: 34 additions & 0 deletions src/main/java/de/app/fivegla/persistence/ImageRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import de.app.fivegla.persistence.entity.Group;
import de.app.fivegla.persistence.entity.Image;
import de.app.fivegla.persistence.entity.enums.ImageChannel;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -43,4 +46,35 @@ public interface ImageRepository extends JpaRepository<Image, Long> {
* @param group The group to filter the images by.
*/
List<Image> findByGroup(Group group);

/**
* Finds images by transaction id and channel.
*
* @param transactionId The transaction id.
* @param channel The channel of the image.
* @param tenantId The id of the tenant.
* @return The images with the given transaction id and channel.
*/
List<Image> findByTransactionIdAndChannelAndTenantTenantId(String transactionId, ImageChannel channel, String tenantId);

/**
* Finds all transaction IDs within a specified time frame.
*
* @param tenantId The ID of the tenant.
* @param from The start of the time frame.
* @param to The end of the time frame.
* @return A list of transaction IDs.
*/
@Query("SELECT DISTINCT(i.transactionId) FROM Image i WHERE i.tenant.tenantId = :tenantId AND i.measuredAt >= :from AND i.measuredAt <= :to")
List<String> findAllTransactionIdsWithinTimeFrame(String tenantId, Instant from, Instant to);


/**
* Finds the first image with the specified transaction ID ordered by the measuredAt in ascending order.
*
* @param transactionId The transaction ID to search for.
* @return The first image with the specified transaction ID ordered by the measuredAt in ascending order.
*/
Image findFirstByTransactionIdOrderByMeasuredAtAsc(String transactionId);

}
2 changes: 1 addition & 1 deletion src/main/java/de/app/fivegla/persistence/entity/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class Image extends BaseEntity {
private String transactionId;

/**
* The channel of the image since the value can not be read from the EXIF.
* The channel of the image since the value cannot be read from the EXIF.
*/
@Column(name = "image_channel", nullable = false)
@Enumerated(EnumType.STRING)
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
app:
api-key: ${API_KEY}
imagePathBaseUrl: ${IMAGE_PATH_BASE_URL}
defaultSearchIntervalInDays: 7
scheduled:
data-import:
# Spring Boot uses ISO-8601 durations, https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ app:
version: '@project.version@'
api-key: super_secret_api_key
imagePathBaseUrl: ${IMAGE_PATH_BASE_URL}
defaultSearchIntervalInDays: 7
scheduled:
data-import:
# Spring Boot uses ISO-8601 durations, https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)
Expand Down

0 comments on commit 86bf534

Please sign in to comment.