From 62ccb3b8272c6c40ce537c843dcd223d592ec7ff Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Tue, 6 Sep 2022 11:08:23 +0300 Subject: [PATCH] RED-5047 - concurent report generation --- .../v1/server/model/MultiFileWorkbook.java | 1 + .../service/ReportGenerationService.java | 41 ++---- .../RedactionReportV2IntegrationTest.java | 135 +++++++++++------- .../resources/templates/report-advanced.xlsx | Bin 0 -> 10281 bytes .../src/test/resources/templates/report.xlsx | Bin 0 -> 9494 bytes 5 files changed, 94 insertions(+), 83 deletions(-) create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/test/resources/templates/report-advanced.xlsx create mode 100644 redaction-report-service-v1/redaction-report-service-server-v1/src/test/resources/templates/report.xlsx diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/model/MultiFileWorkbook.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/model/MultiFileWorkbook.java index 83df443..d9e1320 100644 --- a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/model/MultiFileWorkbook.java +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/model/MultiFileWorkbook.java @@ -14,5 +14,6 @@ public class MultiFileWorkbook { private SXSSFWorkbook writeWorkbook; private String templateId; private String templateName; + private ExcelModel excelModel; } diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/ReportGenerationService.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/ReportGenerationService.java index 180957a..629f5e2 100644 --- a/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/ReportGenerationService.java +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/main/java/com/iqser/red/service/redaction/report/v1/server/service/ReportGenerationService.java @@ -1,19 +1,5 @@ package com.iqser.red.service.redaction.report.v1.server.service; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.xssf.streaming.SXSSFWorkbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.apache.poi.xwpf.usermodel.XWPFDocument; -import org.springframework.stereotype.Service; - import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.ReportTemplate; import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.Dossier; import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.file.FileModel; @@ -21,12 +7,7 @@ import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.ty import com.iqser.red.service.redaction.report.v1.api.model.ReportRequestMessage; import com.iqser.red.service.redaction.report.v1.api.model.ReportType; import com.iqser.red.service.redaction.report.v1.api.model.StoredFileInformation; -import com.iqser.red.service.redaction.report.v1.server.client.DictionaryClient; -import com.iqser.red.service.redaction.report.v1.server.client.DossierClient; -import com.iqser.red.service.redaction.report.v1.server.client.FileStatusClient; -import com.iqser.red.service.redaction.report.v1.server.client.RedactionLogClient; -import com.iqser.red.service.redaction.report.v1.server.client.ReportTemplateClient; -import com.iqser.red.service.redaction.report.v1.server.model.ExcelModel; +import com.iqser.red.service.redaction.report.v1.server.client.*; import com.iqser.red.service.redaction.report.v1.server.model.MultiFileDocument; import com.iqser.red.service.redaction.report.v1.server.model.MultiFileWorkbook; import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel; @@ -35,11 +16,19 @@ import com.iqser.red.service.redaction.report.v1.server.settings.ReportTemplateS import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist; - import io.micrometer.core.annotation.Timed; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.*; @SuppressWarnings("ALL") @Slf4j @@ -82,7 +71,8 @@ public class ReportGenerationService { for (Sheet sheet : readWorkbook) { writeWorkbook.createSheet(sheet.getSheetName()); } - MultiFileWorkbook multiFileWorkbook = new MultiFileWorkbook(readWorkbook, writeWorkbook, templateId, reportTemplate.getFileName()); + MultiFileWorkbook multiFileWorkbook = new MultiFileWorkbook(readWorkbook, writeWorkbook, templateId, reportTemplate.getFileName(), + excelTemplateReportGenerationService.calculateExcelModel(readWorkbook.getSheetAt(0))); multiFileWorkbooks.add(multiFileWorkbook); } catch (IOException e) { throw new RuntimeException("Could not generate multifile excel report."); @@ -107,7 +97,6 @@ public class ReportGenerationService { var placeholderModel = generatePlaceholderService.buildPlaceholders(dossier); - ExcelModel excelModel = null; int i = 1; for (int j = 0; j < reportMessage.getFileIds().size(); j++) { @@ -124,11 +113,7 @@ public class ReportGenerationService { var isLastFile = j == reportMessage.getFileIds().size() - 1; for (MultiFileWorkbook multiFileWorkbook : multiFileWorkbooks) { - if (excelModel == null) { - excelModel = excelTemplateReportGenerationService.calculateExcelModel(multiFileWorkbook.getReadWorkBook() - .getSheetAt(0)); - } - excelTemplateReportGenerationService.generateExcelReport(reportEntries, placeholderModel, multiFileWorkbook.getTemplateName(), multiFileWorkbook.getWriteWorkbook(), dossier.getDossierName(), fileStatus, excelModel, isLastFile); + excelTemplateReportGenerationService.generateExcelReport(reportEntries, placeholderModel, multiFileWorkbook.getTemplateName(), multiFileWorkbook.getWriteWorkbook(), dossier.getDossierName(), fileStatus, multiFileWorkbook.getExcelModel(), isLastFile); } for (MultiFileDocument multiFileDocument : multiFileDocuments) { diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/RedactionReportV2IntegrationTest.java b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/RedactionReportV2IntegrationTest.java index b7d00f7..13e5a66 100644 --- a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/RedactionReportV2IntegrationTest.java +++ b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/java/com/iqser/red/service/redaction/report/v1/server/RedactionReportV2IntegrationTest.java @@ -1,31 +1,5 @@ package com.iqser.red.service.redaction.report.v1.server; -import static com.iqser.red.service.redaction.report.v1.server.utils.OsUtils.getTemporaryDirectory; -import static org.mockito.Mockito.when; - -import java.io.FileOutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.io.IOUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.junit4.SpringRunner; - import com.amazonaws.services.s3.AmazonS3; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,23 +13,40 @@ import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.do import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.file.FileModel; import com.iqser.red.service.redaction.report.v1.api.model.ReportRequestMessage; import com.iqser.red.service.redaction.report.v1.api.model.StoredFileInformation; -import com.iqser.red.service.redaction.report.v1.server.client.DictionaryClient; -import com.iqser.red.service.redaction.report.v1.server.client.DossierAttributesClient; -import com.iqser.red.service.redaction.report.v1.server.client.DossierAttributesConfigClient; -import com.iqser.red.service.redaction.report.v1.server.client.DossierClient; -import com.iqser.red.service.redaction.report.v1.server.client.FileAttributesConfigClient; -import com.iqser.red.service.redaction.report.v1.server.client.FileStatusClient; -import com.iqser.red.service.redaction.report.v1.server.client.RedactionLogClient; -import com.iqser.red.service.redaction.report.v1.server.client.ReportTemplateClient; +import com.iqser.red.service.redaction.report.v1.server.client.*; import com.iqser.red.service.redaction.report.v1.server.configuration.MessagingConfiguration; import com.iqser.red.service.redaction.report.v1.server.service.ReportGenerationService; import com.iqser.red.service.redaction.report.v1.server.utils.FileSystemBackedStorageService; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.storage.commons.StorageAutoConfiguration; import com.iqser.red.storage.commons.service.StorageService; - import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.*; +import org.springframework.core.io.ClassPathResource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.Base64Utils; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static com.iqser.red.service.redaction.report.v1.server.utils.OsUtils.getTemporaryDirectory; +import static org.mockito.Mockito.when; + +@Slf4j @RunWith(SpringRunner.class) @Import(RedactionReportV2IntegrationTest.TestConfiguration.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -103,27 +94,15 @@ public class RedactionReportV2IntegrationTest { @Autowired private FileSystemBackedStorageService fileSystemBackedStorageService; - @Test - @SneakyThrows - public void testEntireReportFlow() { + @SneakyThrows + private ReportRequestMessage prepareFlow(String... templates) { var testDossier = new Dossier(); testDossier.setDossierName("Test Dossier"); testDossier.setDossierTemplateId("dossierTemplateId"); testDossier.setId("dossierId"); when(dossierClient.getDossierById("dossierId", true, false)).thenReturn(testDossier); - ReportTemplate testTemplate = new ReportTemplate(); - testTemplate.setDossierTemplateId("dossierTemplateId"); - testTemplate.setTemplateId("templateId"); - testTemplate.setFileName("test-template.xlsx"); - testTemplate.setStorageId("templateStorageId"); - testTemplate.setMultiFileReport(true); - when(reportTemplateClient.getReportTemplate("dossierTemplateId", "templateId")).thenReturn(testTemplate); - - ClassPathResource templateResource = new ClassPathResource("templates/Excel Report.xlsx"); - fileSystemBackedStorageService.storeObject("templateStorageId", templateResource.getInputStream()); - var testDossierAttributes = new ArrayList(); testDossierAttributes.add(DossierAttribute.builder().dossierId("dossierId").dossierAttributeConfigId("testDossierAttribute").value("Dossier Attribute Value").build()); @@ -134,7 +113,6 @@ public class RedactionReportV2IntegrationTest { .placeholder("{{dossier.attribute.TestDossierAttribute}}").dossierTemplateId("dossierTemplateId").id("testDossierAttribute").build()); when(dossierAttributesConfigClient.getDossierAttributes("dossierTemplateId")).thenReturn(dossierAttributeConfig); - var fileAttributeConfig = new ArrayList(); fileAttributeConfig.add(FileAttributeConfig.builder().type(FileAttributeType.TEXT) .placeholder("{{file.attribute.TestFileAttribute}}").dossierTemplateId("dossierTemplateId").id("testFileAttribute").build()); @@ -148,29 +126,76 @@ public class RedactionReportV2IntegrationTest { when(redactionLogClient.getRedactionLog("dossierId", "fileId", new ArrayList<>(), true, false)).thenReturn(redactionLog); + + var templateIds = new HashSet(); + for (var template : templates) { + var templateId = Base64Utils.encodeToString(template.getBytes(StandardCharsets.UTF_8)); + templateIds.add(templateId); + log.info("Assigned template Id {} to file {}", templateId, template); + ReportTemplate testTemplate = new ReportTemplate(); + testTemplate.setDossierTemplateId("dossierTemplateId"); + testTemplate.setTemplateId(templateId); + testTemplate.setFileName(template); + testTemplate.setStorageId(templateId + "Storage"); + testTemplate.setMultiFileReport(true); + when(reportTemplateClient.getReportTemplate("dossierTemplateId", templateId)).thenReturn(testTemplate); + + ClassPathResource templateResource = new ClassPathResource(template); + fileSystemBackedStorageService.storeObject(templateId + "Storage", templateResource.getInputStream()); + + + } + + var request = new ReportRequestMessage(); request.setDossierId("dossierId"); request.setFileIds(List.of("fileId")); request.setDossierTemplateId("dossierTemplateId"); request.setDownloadId("downloadId"); request.setUserId("userId"); - request.setTemplateIds(Set.of("templateId")); - var id = reportGenerationService.generateReports(request); + request.setTemplateIds(templateIds); + + return request; + } + + @SneakyThrows + private void processRequest(ReportRequestMessage reportRequestMessage) { + var id = reportGenerationService.generateReports(reportRequestMessage); var object = fileSystemBackedStorageService.getObject(id); - var infoList =objectMapper.readValue(object.getInputStream(), new TypeReference>() { + var infoList = objectMapper.readValue(object.getInputStream(), new TypeReference>() { }); - for(var fileInfo: infoList) { + int i = 1; + + for (var fileInfo : infoList) { var report = fileSystemBackedStorageService.getObject(fileInfo.getStorageId()); byte[] reportInfo = IOUtils.toByteArray(report.getInputStream()); - try (FileOutputStream fileOutputStream = new FileOutputStream(getTemporaryDirectory() + "/output.xlsx")) { + try (FileOutputStream fileOutputStream = new FileOutputStream(getTemporaryDirectory() + "/output-" + fileInfo.getTemplateId() + ".xlsx")) { fileOutputStream.write(reportInfo); } + System.out.println("Created temporary output file: " + getTemporaryDirectory() + "/output-" + fileInfo.getTemplateId() + ".xlsx"); + i++; } } + @Test + @SneakyThrows + public void testBasicExcelReportFlow() { + + var reportRequestMessage = prepareFlow("templates/Excel Report.xlsx"); + processRequest(reportRequestMessage); + } + + @Test + @SneakyThrows + public void test2FilesConcurrently() { + + var reportRequestMessage = prepareFlow("templates/report.xlsx", "templates/report-advanced.xlsx"); + processRequest(reportRequestMessage); + } + @Configuration @EnableAutoConfiguration(exclude = {StorageAutoConfiguration.class, RabbitAutoConfiguration.class}) @ComponentScan("com.iqser.red.service.persistence") diff --git a/redaction-report-service-v1/redaction-report-service-server-v1/src/test/resources/templates/report-advanced.xlsx b/redaction-report-service-v1/redaction-report-service-server-v1/src/test/resources/templates/report-advanced.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..25618ffb28acea5f52840d86ff1864af5d27ca54 GIT binary patch literal 10281 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 R-~s&i8h$2tP50Zs{{tOQr)K~F literal 0 HcmV?d00001