diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java index e72a60c599aa61d2e0c47e27c1e24546b8670e3c..c0c1f70a570be4dd12c3c0b9ecfea8af87ea746b 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java @@ -2,19 +2,13 @@ package de.ozgcloud.nachrichten.postfach.osiv2; import java.util.stream.Stream; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - import de.ozgcloud.nachrichten.postfach.PostfachNachricht; import de.ozgcloud.nachrichten.postfach.PostfachRemoteService; -import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; import de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2PostfachService; -import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @Log4j2 @RequiredArgsConstructor public class OsiPostfachRemoteService implements PostfachRemoteService { diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java new file mode 100644 index 0000000000000000000000000000000000000000..2aa3c2cd3edc7411b253969f47b35eef4e67cd3d --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java @@ -0,0 +1,11 @@ +package de.ozgcloud.nachrichten.postfach.osiv2; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; + +@Service +@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +public @interface ServiceIfOsi2Enabled { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/UploadFileMetadata.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java similarity index 53% rename from src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/UploadFileMetadata.java rename to src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java index e1b6ead85e65860e4b15ad6798aaf37083658294..330c80eb3392aaecd9ced6732c41e1fa4d65a7dd 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/UploadFileMetadata.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java @@ -3,10 +3,12 @@ package de.ozgcloud.nachrichten.postfach.osiv2.model; import lombok.Builder; @Builder -public record UploadFileMetadata( +public record FileChunkInfo( String guid, - String name, + String fileName, String contentType, - Long size + Integer chunkIndex, + Integer totalChunks, + Long totalFileSize ) { } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkResource.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkResource.java new file mode 100644 index 0000000000000000000000000000000000000000..59078e50a3ada9e039d40e56dd2d8a774d4b8611 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkResource.java @@ -0,0 +1,28 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +import org.springframework.core.io.AbstractResource; + +import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +@Builder +@RequiredArgsConstructor +public class FileChunkResource extends AbstractResource { + private final Integer chunkSize; + private final ByteBuffer content; + + @Override + public String getDescription() { + return "Chunk of a file"; + } + + @Override + public InputStream getInputStream() { + return new ByteBufferBackedInputStream(content); + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java new file mode 100644 index 0000000000000000000000000000000000000000..16e1ec1a3ec8e817861355abc7320cb7168b05d9 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java @@ -0,0 +1,11 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import lombok.Builder; + +@Builder +public record Osi2FileUpload( + String guid, + OzgCloudFile file +) { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStream.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..23bf405cfac53ca242cae1811d1c7d3b5cdcefb9 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStream.java @@ -0,0 +1,57 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.storage; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class ConcatInputStream extends InputStream { + private final Iterator<? extends InputStream> streams; + + private InputStream currentStream = null; + + @Override + public int read() throws IOException { + var buffer = new byte[1]; + var result = read(buffer, 0, 1); + return result < 0 ? result : buffer[0]; + } + + private boolean useNextStreamFails() throws IOException { + close(); + if (streams.hasNext()) { + currentStream = streams.next(); + return false; + } + return true; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (currentStream == null && useNextStreamFails()) { + return -1; + } + var remainingLength = length; + while (remainingLength > 0) { + var readLength = currentStream.read(buffer, offset, remainingLength); + if (readLength > 0) { + remainingLength -= readLength; + offset += readLength; + } else if (useNextStreamFails()) { + break; + } + } + var totalReadLength = length - remainingLength; + return totalReadLength >= 0 ? totalReadLength : -1; + } + + @Override + public void close() throws IOException { + if (currentStream != null) { + currentStream.close(); + currentStream = null; + } + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..5b8073c96e899d238a89d2c807c108a6001d8e02 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileMapper.java @@ -0,0 +1,18 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.storage; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface Osi2BinaryFileMapper { + + OzgCloudFile mapToUploadFileMetadata(GrpcOzgFile grpcOzgFile); + + default OzgCloudFileId mapToUploadFileId(String fileId) { + return new OzgCloudFileId(fileId); + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileRemoteService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..efa3a16c71ee5d51997d304c6bd60a6af82cceed --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/Osi2BinaryFileRemoteService.java @@ -0,0 +1,52 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.storage; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; +import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcBinaryFilesRequest; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcGetBinaryFileDataRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@ServiceIfOsi2Enabled +@RequiredArgsConstructor +public class Osi2BinaryFileRemoteService { + private final BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFileService; + private final Osi2BinaryFileMapper binaryFileMapper; + + public List<OzgCloudFile> getFileMetadataOfIds(List<String> fileIds) { + return binaryFileService.findBinaryFilesMetaData(createRequestWithFileIds(fileIds)) + .getFileList() + .stream() + .map(binaryFileMapper::mapToUploadFileMetadata) + .toList(); + } + + private GrpcBinaryFilesRequest createRequestWithFileIds(List<String> fileIds) { + return GrpcBinaryFilesRequest.newBuilder().addAllFileId(fileIds).build(); + } + + public InputStream streamFileContent(String fileId) { + var response = binaryFileService.getBinaryFileContent(createRequestWithFileId(fileId)); + return new ConcatInputStream(new Iterator<>() { + @Override + public boolean hasNext() { + return response.hasNext(); + } + + @Override + public InputStream next() { + return response.next().getFileContent().newInput(); + } + }); + } + + private GrpcGetBinaryFileDataRequest createRequestWithFileId(String fileId) { + return GrpcGetBinaryFileDataRequest.newBuilder().setFileId(fileId).build(); + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java index 372704e2de6dc99884053c3488d96604cb9fa9d7..a53021ee35450f1317f49d1fac9fc9064889a6ba 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java @@ -3,16 +3,17 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import java.util.stream.Stream; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Log4j2 -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @RequiredArgsConstructor public class Osi2PostfachService { private final PostfachApiFacadeService postfachApiFacadeService; diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java index 5f18c4d64a6634678adb3b6ff9b03f79f30a6080..1694d6db9409d53ecd4e0991c068a40bcb3ba956 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java @@ -1,42 +1,51 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import java.time.Duration; +import java.util.Comparator; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BooleanSupplier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - -import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; -import de.ozgcloud.nachrichten.postfach.osiv2.model.UploadFileMetadata; -import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc; -import de.ozgcloud.vorgang.grpc.binaryFile.GrpcGetBinaryFileDataRequest; +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; +import de.ozgcloud.nachrichten.postfach.osiv2.storage.Osi2BinaryFileRemoteService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Log4j2 -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @RequiredArgsConstructor public class Osi2QuarantineService { private final PostfachApiFacadeService postfachApiFacadeService; - private final BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFileService; + private final Osi2BinaryFileRemoteService binaryFileService; - public List<UploadFileMetadata> uploadAttachments(List<String> attachmentIds) { - var uploadFileMetadata = attachmentIds.stream() - .map(this::uploadFileByAttachmentId) - .toList(); - waitForVirusScan(uploadFileMetadata); - return uploadFileMetadata; - } + static final Duration POLLING_INTERVAL = Duration.ofSeconds(1); + static final Duration POLLING_TIMEOUT = Duration.ofMinutes(5); - void waitForVirusScan(List<UploadFileMetadata> uploadFileMetadata) { - // TODO + public List<Osi2FileUpload> uploadAttachments(List<String> attachmentIds) { + var ozgCloudFiles = binaryFileService.getFileMetadataOfIds(attachmentIds); + var uploadFiles = uploadFilesToQuarantineWithLargestFileFirst(ozgCloudFiles); + waitForVirusScan(uploadFiles); + return uploadFiles; } - void checkVirusScanCompleted(List<UploadFileMetadata> uploadFileMetadata) { - // TODO + List<Osi2FileUpload> uploadFilesToQuarantineWithLargestFileFirst(List<OzgCloudFile> ozgCloudFiles) { + return ozgCloudFiles.stream() + .sorted(Comparator.comparing(OzgCloudFile::getSize).reversed()) + .map(this::uploadFileToQuarantine) + .toList(); } - UploadFileMetadata uploadFileByAttachmentId(String attachmentId) { + + Osi2FileUpload uploadFileToQuarantine(OzgCloudFile file) { +// binaryFileService.streamFileContent(file.getId().toString()) +// .forEachRemaining(chunk -> postfachApiFacadeService.uploadChunk(file.getId(), chunk.getFileContent().toByteArray())); // TODO // var request = createRequestWithFileId(attachmentId); // var metadata = binaryFileService.findBinaryFilesMetaData(GrpcBinaryFilesRequest.newBuilder().) @@ -46,11 +55,41 @@ public class Osi2QuarantineService { return null; } - private GrpcGetBinaryFileDataRequest createRequestWithFileId(String fileId) { - return GrpcGetBinaryFileDataRequest.newBuilder().setFileId(fileId).build(); + + + synchronized boolean checkVirusScanCompleted(List<Osi2FileUpload> osi2FileMetadata) { + // TODO + return true; + } + + void waitForVirusScan(List<Osi2FileUpload> osi2FileMetadata) { + try { + waitUntil(() -> checkVirusScanCompleted(osi2FileMetadata), POLLING_INTERVAL, POLLING_TIMEOUT); + } catch (ExecutionException e) { + throw new OsiPostfachException("Expect the scan to complete successfully!", e.getCause()); + } catch (TimeoutException e) { + throw new OsiPostfachException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds()), e); + } catch (InterruptedException e) { + LOG.error("[waitForVirusScan] Interrupt"); + Thread.currentThread().interrupt(); + } } - public void deleteAttachments(List<UploadFileMetadata> uploadFileMetadata) { + void waitUntil(BooleanSupplier condition, Duration interval, Duration timeout) + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture.runAsync(() -> { + try (var executor = Executors.newSingleThreadScheduledExecutor()) { + executor.scheduleAtFixedRate(() -> { + if (condition.getAsBoolean()) { + executor.shutdown(); + } + }, 0, interval.getSeconds(), TimeUnit.SECONDS); + } + }).get(timeout.getSeconds(), TimeUnit.SECONDS); + } + + + public void deleteAttachments(List<Osi2FileUpload> osi2FileMetadata) { // TODO delete on exception } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java index bf883afec4d1e4ad65c89198b3eb1c704bc48ed1..9ed7845cdf8e1530428a290b2b943048f148182d 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java @@ -2,14 +2,14 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; -import org.mapstruct.BeforeMapping; -import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Named; import org.mapstruct.ReportingPolicy; import de.ozgcloud.nachrichten.postfach.PostfachAddress; @@ -20,7 +20,8 @@ import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeFiles; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1References; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior; -import de.ozgcloud.nachrichten.postfach.osiv2.model.UploadFileMetadata; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) public interface Osi2RequestMapper { @@ -35,14 +36,33 @@ public interface Osi2RequestMapper { @Mapping(target = "eidasLevel", constant = "LOW") @Mapping(target = "isObligatory", expression = "java( false )") @Mapping(target = "isHtml", expression = "java( false )") - @Mapping(target = "files", source = "files") + @Mapping(target = "files", expression = "java( mapMessageExchangeFiles(nachricht, files) )") @Mapping(target = "references", expression = "java( mapReferences() )") - OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht, List<UploadFileMetadata> files); + OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht, List<Osi2FileUpload> files); + default List<MessageExchangeFiles> mapMessageExchangeFiles(PostfachNachricht nachricht, List<Osi2FileUpload> files) { + var filesById = associateUploadWithAttachmentId(files); + return nachricht.getAttachments() + .stream() + .map(fileId -> Objects.requireNonNull( + filesById.get(fileId), + "Expect all attachmentIds are uploaded!" + )) + .map(this::mapMessageExchangeFile) + .toList(); + } + + default Map<String, Osi2FileUpload> associateUploadWithAttachmentId(List<Osi2FileUpload> uploads) { + return uploads.stream() + .filter(upload -> upload.file() != null && upload.file().getId() != null) + .collect(Collectors.toMap(upload -> upload.file().getId().toString(), Function.identity())); + } - @Mapping(target = "mimeType", source = "contentType") + @Mapping(target = "mimeType", source = "file.contentType") + @Mapping(target = "name", source = "file.name") + @Mapping(target = "size", source = "file.size") @Mapping(target = "isOriginalMessage", expression = "java( false )") - MessageExchangeFiles mapMessageExchangeFile(UploadFileMetadata attachmentMetadata); + MessageExchangeFiles mapMessageExchangeFile(Osi2FileUpload fileUpload); default List<V1References> mapReferences() { return Collections.emptyList(); @@ -56,8 +76,9 @@ public interface Osi2RequestMapper { }; } - - DomainChunkMetaData mapDomainChunkMetaData(UploadFileMetadata attachmentMetadata); + @Mapping(target = "target", ignore = true) + @Mapping(target = "uploadUid", source = "guid") + DomainChunkMetaData mapDomainChunkMetaData(FileChunkInfo fileChunkInfo); default String mapMailboxId(PostfachNachricht nachricht) { return Optional.ofNullable(nachricht.getPostfachAddress()) diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java index 153b5ebdb9cb5aea52bdcafe56b714f008a622da..e4d6b9b9802b41b45c8f1bdf26f48af22ae06479 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java @@ -5,20 +5,19 @@ import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2RequestMapper. import java.util.List; import java.util.UUID; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi; -import de.ozgcloud.nachrichten.postfach.osiv2.model.UploadFileMetadata; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkResource; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Log4j2 -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @RequiredArgsConstructor public class PostfachApiFacadeService { @@ -28,20 +27,20 @@ public class PostfachApiFacadeService { private final Osi2ResponseMapper responseMapper; private final Osi2PostfachProperties.ApiConfiguration apiConfiguration; - public void sendMessage(PostfachNachricht nachricht, List<UploadFileMetadata> attachments) { + public void sendMessage(PostfachNachricht nachricht, List<Osi2FileUpload> attachments) { messageExchangeApi.sendMessage( requestMapper.mapMailboxId(nachricht), requestMapper.mapOutSendMessageRequestV2(nachricht, attachments) ); } - public void uploadChunk(String uploadId, byte[] chunk) { + public void uploadChunk(FileChunkInfo chunkInfo, FileChunkResource chunkResource) { // TODO // quarantineApi.uploadChunk( - // requestMapper.mapDomainChunkMetaData(null), + // requestMapper.mapDomainChunkMetaData(chunkInfo), // apiConfiguration.getTenant(), // apiConfiguration.getNameIdentifier(), - // new ByteArrayResource(chunk) + // chunkResource // ); } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStreamTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStreamTest.java new file mode 100644 index 0000000000000000000000000000000000000000..56ee839e66d80a5ff4ede515aebc841b3c29241d --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/ConcatInputStreamTest.java @@ -0,0 +1,78 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.storage; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import lombok.SneakyThrows; + +class ConcatInputStreamTest { + + @DisplayName("should concatenate input streams") + @Test + @SneakyThrows + void shouldConcatenateInputStreams() { + var concatStream = createExampleConcatInputStream(); + + var result = IOUtils.toByteArray(concatStream); + + assertEquals("123456789", new String(result)); + } + + @DisplayName("should read one") + @Test + @SneakyThrows + void shouldReadOne() { + var concatStream = createExampleConcatInputStream(); + + var result = concatStream.read(); + + assertThat(result).isEqualTo('1'); + } + + @DisplayName("should write to buffer with offset") + @Test + @SneakyThrows + void shouldWriteToBufferWithOffset() { + var buffer = new byte[10]; + var concatStream = createExampleConcatInputStream(); + + var result = concatStream.read(buffer, 2, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '2', 0, 0, 0, 0, 0, 0); + + result = concatStream.read(buffer, 3, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '4', 0, 0, 0, 0, 0); + + result = concatStream.read(buffer, 4, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '6', 0, 0, 0, 0); + + result = concatStream.read(buffer, 5, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '8', 0, 0, 0); + + result = concatStream.read(buffer, 6, 2); + assertThat(result).isEqualTo(1); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '9', 0, 0, 0); + + result = concatStream.read(buffer, 7, 2); + assertThat(result).isEqualTo(-1); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '9', 0, 0, 0); + } + + private ConcatInputStream createExampleConcatInputStream() { + var streams = Stream.of("1234","567","", "89") + .map(s -> new ByteArrayInputStream(s.getBytes())) + .toList(); + return new ConcatInputStream(streams.iterator()); + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9a79726e6e085478515751817de3f612f0e243d3 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java @@ -0,0 +1,32 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import java.util.UUID; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; + +public class Osi2FileUploadTestFactory { + + public static final String UPLOAD_FILE_ID = UUID.randomUUID().toString(); + public static final String UPLOAD_GUID = UUID.randomUUID().toString(); + public static final String UPLOAD_GUID2 = UUID.randomUUID().toString(); + public static final String UPLOAD_NAME = "upload.txt"; + public static final String UPLOAD_CONTENT_TYPE = "text/plain"; + public static final Long UPLOAD_SIZE = 1001L; + + public static Osi2FileUpload create() { + return createBuilder().build(); + } + + public static Osi2FileUpload.Osi2FileUploadBuilder createBuilder() { + return Osi2FileUpload.builder() + .guid(UPLOAD_GUID) + .file(OzgCloudFile.builder() + .id(new OzgCloudFileId(UPLOAD_FILE_ID)) + .name(UPLOAD_NAME) + .contentType(UPLOAD_CONTENT_TYPE) + .size(UPLOAD_SIZE) + .build()); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6e26bd2f501d0391a5fbf741d238e53638c74e1f --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java @@ -0,0 +1,94 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; + +class Osi2PostfachServiceTest { + + @InjectMocks + private Osi2PostfachService osi2PostfachService; + + @Mock + private PostfachApiFacadeService postfachApiFacadeService; + @Mock + private Osi2QuarantineService quarantineService; + + @DisplayName("send message") + @Nested + class TestSendMessage { + private PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); + private List<Osi2FileUpload> uploadFiles = List.of(Osi2FileUploadTestFactory.create()); + + @BeforeEach + void mock() { + when(quarantineService.uploadAttachments(any())).thenReturn(uploadFiles); + } + + @DisplayName("should send message") + @Test + void shouldSendMessage() { + osi2PostfachService.sendMessage(nachricht); + + verify(postfachApiFacadeService).sendMessage(nachricht, uploadFiles); + } + + @DisplayName("should upload attachments of nachricht") + @Test + void shouldUploadAttachmentsOfNachricht() { + osi2PostfachService.sendMessage(nachricht); + + verify(quarantineService).uploadAttachments(nachricht.getAttachments()); + } + } + + @DisplayName("receive messages") + @Nested + class TestReceiveMessage { + + @DisplayName("with two pending messages") + @Nested + class TestWithTwoPendingMessages { + + @BeforeEach + void mock() { + when(postfachApiFacadeService.fetchPendingMessageIds()).thenReturn(List.of(MESSAGE_ID_1, MESSAGE_ID_2)); + when(postfachApiFacadeService.fetchMessageById(MESSAGE_ID_1)) + .thenReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build()); + when(postfachApiFacadeService.fetchMessageById(MESSAGE_ID_2)) + .thenReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build()); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var messages = receiveMessages(); + + assertThat(messages) + .extracting(PostfachNachricht::getMessageId) + .containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + } + + } + + private Stream<PostfachNachricht> receiveMessages() { + return osi2PostfachService.receiveMessages(); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java index 3136b93aa50edef92e299c2f727194550e3d08ec..c36203392fc1a319d39de36d41599518067aa637 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java @@ -2,7 +2,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; -import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.UploadFileMetadataTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2FileUploadTestFactory.*; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; @@ -18,6 +18,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mapstruct.factory.Mappers; +import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; import de.ozgcloud.nachrichten.postfach.PostfachAddress; import de.ozgcloud.nachrichten.postfach.PostfachAddressIdentifier; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; @@ -175,13 +176,17 @@ class Osi2RequestMapperTest { @Test void shouldMapTwoFiles() { var files = List.of( - UploadFileMetadataTestFactory.create(), - UploadFileMetadataTestFactory.createBuilder() + Osi2FileUploadTestFactory.create(), + Osi2FileUploadTestFactory.createBuilder() + .file(OzgCloudFileTestFactory.create()) .guid(UPLOAD_GUID2) .build() ); + var nachricht = PostfachNachrichtTestFactory.createBuilder() + .attachments(List.of(UPLOAD_FILE_ID, OzgCloudFileTestFactory.ID_STR)) + .build(); - var result = mapper.mapOutSendMessageRequestV2(PostfachNachrichtTestFactory.create(), files); + var result = mapper.mapOutSendMessageRequestV2(nachricht, files); assertThat(result.getFiles()) .usingRecursiveComparison() @@ -193,7 +198,7 @@ class Osi2RequestMapperTest { void shouldMapFiles() { var result = mapper.mapOutSendMessageRequestV2( PostfachNachrichtTestFactory.create(), - List.of(UploadFileMetadataTestFactory.create()) + List.of(Osi2FileUploadTestFactory.create()) ); assertThat(result.getFiles()).isEmpty(); @@ -220,7 +225,7 @@ class Osi2RequestMapperTest { void shouldMapGuid() { var result = mapFile(); - assertThat(result.getGuid()).isEqualTo(UPLOAD_GUID); + assertThat(result.getGuid()).isEqualTo(UUID.fromString(UPLOAD_GUID)); } @DisplayName("should map mimeType") @@ -248,7 +253,7 @@ class Osi2RequestMapperTest { } private MessageExchangeFiles mapFile() { - return mapper.mapMessageExchangeFile(UploadFileMetadataTestFactory.create()); + return mapper.mapMessageExchangeFile(Osi2FileUploadTestFactory.create()); } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java index 395f286e2fe85c7fc49e77469480b78eb5931593..faf012b0373acb1ea8d98e359de78379df7b8852 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java @@ -2,7 +2,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory.*; -import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2RequestMapper.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -18,16 +18,15 @@ import org.mockito.Mock; import org.mockito.Spy; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; -import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachException; import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeDeleteMessageResponse; -import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessagesResponse; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeSendMessageResponse; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; class PostfachApiFacadeServiceTest { @@ -54,28 +53,29 @@ class PostfachApiFacadeServiceTest { @Mock MessageExchangeSendMessageResponse messageExchangeSendMessageResponse; + List<Osi2FileUpload> files = List.of(Osi2FileUploadTestFactory.create()); + private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); @BeforeEach void mock() { when(osi2RequestMapper.mapMailboxId(nachricht)).thenReturn(MAILBOX_ID); - when(osi2RequestMapper.mapOutSendMessageRequestV2(nachricht)).thenReturn(outSendMessageRequestV2); + when(osi2RequestMapper.mapOutSendMessageRequestV2(nachricht, files)).thenReturn(outSendMessageRequestV2); when(messageExchangeApi.sendMessage(any(), any())).thenReturn(messageExchangeSendMessageResponse); } @DisplayName("should call sendMessage") @Test void shouldCallSendMessage() { - postfachApiFacadeService.sendMessage(nachricht); + postfachApiFacadeService.sendMessage(nachricht, files); verify(messageExchangeApi).sendMessage(MAILBOX_ID, outSendMessageRequestV2); - } } - @DisplayName("receive messages") + @DisplayName("fetch pending message ids") @Nested - class TestReceiveMessage { + class TestFetchPendingMessageIds { @DisplayName("with two pending messages") @Nested @@ -87,26 +87,29 @@ class PostfachApiFacadeServiceTest { @BeforeEach void mock() { when(messageExchangeApi.receiveMessages(anyInt(), anyInt())).thenReturn(response); - doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build()) - .when(postfachApiFacadeService).fetchMessageById(response.getMessages().get(0)); - doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build()) - .when(postfachApiFacadeService).fetchMessageById(response.getMessages().get(1)); + when(osi2ResponseMapper.toMessageIds(any())).thenReturn(List.of(MESSAGE_ID_1, MESSAGE_ID_2)); } @DisplayName("should return") @Test void shouldReturn() { - var messages = receiveMessageList(); + var messageIds = fetchMessageIds(); - assertThat(messages) - .extracting(PostfachNachricht::getMessageId) - .containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + assertThat(messageIds).containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + } + + @DisplayName("should call mapper") + @Test + void shouldCallMapper() { + fetchMessageIds(); + + verify(osi2ResponseMapper).toMessageIds(response); } @DisplayName("should call receiveMessages api method") @Test void shouldCallReceiveMessagesApiMethod() { - receiveMessageList(); + fetchMessageIds(); verify(messageExchangeApi).receiveMessages(MAX_NUMBER_RECEIVED_MESSAGES, 0); } @@ -126,45 +129,31 @@ class PostfachApiFacadeServiceTest { @DisplayName("should return") @Test void shouldReturn() { - var messages = receiveMessageList(); + var messages = fetchMessageIds(); assertThat(messages).isEmpty(); } } - @DisplayName("with null response") - @Nested - class TestWithNullResponse { - - @DisplayName("should throw") - @Test - void shouldThrow() { - assertThatThrownBy(TestReceiveMessage.this::receiveMessageList) - .isInstanceOf(OsiPostfachException.class); - } - } - - private List<PostfachNachricht> receiveMessageList() { - return postfachApiFacadeService.fetchPendingMessageIds().toList(); + private List<String> fetchMessageIds() { + return postfachApiFacadeService.fetchPendingMessageIds(); } } - @DisplayName("fetch Message by guid") + @DisplayName("fetch Message by Id") @Nested - class TestFetchMessageByGuid { + class TestFetchMessageById { @Mock V1ReplyMessage replyMessage; - @Mock - MessageExchangeReceiveMessage receiveMessage; @Test void shouldCallGetMessage() { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); - postfachApiFacadeService.fetchMessageById(receiveMessage); + postfachApiFacadeService.fetchMessageById(MESSAGE_ID_1); verify(messageExchangeApi).getMessage(any()); } @@ -174,7 +163,7 @@ class PostfachApiFacadeServiceTest { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create()); - postfachApiFacadeService.fetchMessageById(receiveMessage); + postfachApiFacadeService.fetchMessageById(MESSAGE_ID_1); verify(osi2ResponseMapper).toPostfachNachricht(any()); } @@ -184,7 +173,7 @@ class PostfachApiFacadeServiceTest { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create()); - var postfachNachricht = postfachApiFacadeService.fetchMessageById(receiveMessage); + var postfachNachricht = postfachApiFacadeService.fetchMessageById(MESSAGE_ID_1); assertThat(postfachNachricht).isInstanceOf(PostfachNachricht.class); } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/UploadFileMetadataTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/UploadFileMetadataTestFactory.java deleted file mode 100644 index c8be39ba431578990396418ae48a0b216dea3dbb..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/UploadFileMetadataTestFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.ozgcloud.nachrichten.postfach.osiv2.transfer; - -import java.util.UUID; - -import de.ozgcloud.nachrichten.postfach.osiv2.model.UploadFileMetadata; - -public class UploadFileMetadataTestFactory { - - public static final String UPLOAD_GUID = UUID.randomUUID().toString(); - public static final String UPLOAD_GUID2 = UUID.randomUUID().toString(); - public static final String UPLOAD_NAME = "upload.txt"; - public static final String UPLOAD_CONTENT_TYPE = "text/plain"; - public static final Long UPLOAD_SIZE = 1001L; - - public static UploadFileMetadata create() { - return createBuilder().build(); - } - - public static UploadFileMetadata.UploadFileMetadataBuilder createBuilder() { - return UploadFileMetadata.builder() - .guid(UPLOAD_GUID) - .name(UPLOAD_NAME) - .contentType(UPLOAD_CONTENT_TYPE) - .size(UPLOAD_SIZE); - } -}