Merge branch 'RED-8651' into 'master'

RED-8651 - Improve report generation performance

Closes RED-8651

See merge request redactmanager/redaction-report-service!64
This commit is contained in:
Andrei Isvoran 2024-04-08 14:04:28 +02:00
commit dba91f73b1
13 changed files with 553 additions and 218 deletions

View File

@ -14,6 +14,12 @@ public interface ReportTemplateResource {
String REPORT_TEMPLATE_PATH = "/report-templates";
String REPORT_TEMPLATE_UPLOAD_PATH = "/upload-template";
String TEMPLATE_ID = "templateId";
String TEMPLATE_ID_PATH_VARIABLE = "/template-id/{" + TEMPLATE_ID + "}";
String DOSSIER_TEMPLATE_ID = "dossierTemplateId";
String DOSSIER_TEMPLATE_ID_PATH_VARIABLE = "/dossier-template-id/{" + DOSSIER_TEMPLATE_ID + "}";
@ -22,4 +28,8 @@ public interface ReportTemplateResource {
@PostMapping(value = REPORT_TEMPLATE_PATH + DOSSIER_TEMPLATE_ID_PATH_VARIABLE, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
List<ReportTemplate> getReportTemplatesByPlaceholder(@PathVariable(DOSSIER_TEMPLATE_ID) String dossierTemplateId, @RequestBody JSONPrimitive<String> placeholder);
@PostMapping(value = REPORT_TEMPLATE_UPLOAD_PATH + TEMPLATE_ID_PATH_VARIABLE)
void uploadTemplate(@PathVariable(TEMPLATE_ID) String templateId);
}

View File

@ -47,6 +47,8 @@ dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.0.4")
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
implementation("org.springframework.boot:spring-boot-starter-logging")
implementation("ch.qos.logback:logback-classic")

View File

@ -10,6 +10,7 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSON
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import com.iqser.red.service.redaction.report.v1.api.resource.ReportTemplateResource;
import com.iqser.red.service.redaction.report.v1.server.service.PlaceholderService;
import com.iqser.red.service.redaction.report.v1.server.utils.TemplateCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -28,4 +29,16 @@ public class ReportTemplateController implements ReportTemplateResource {
return placeholderService.getReportTemplatesByPlaceholder(dossierTemplateId, placeholder.getValue());
}
/**
* If a template with the same id is uploaded we evict it from the cache.
*
* @param templateId The id for the uploaded template
*/
@Override
public void uploadTemplate(String templateId) {
TemplateCache.evictCache(templateId);
}
}

View File

@ -0,0 +1,19 @@
package com.iqser.red.service.redaction.report.v1.server.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
public final class ReportTemplatesModel {
List<ReportTemplate> singleFilesTemplates = Collections.synchronizedList(new ArrayList<>());
List<MultiFileWorkbook> multiFileWorkbookReportTemplates = Collections.synchronizedList(new ArrayList<>());
List<MultiFileDocument> multiFileDocumentReportTemplates = Collections.synchronizedList(new ArrayList<>());
}

View File

@ -32,7 +32,6 @@ import static com.iqser.red.service.redaction.report.v1.server.service.Placehold
import static com.iqser.red.service.redaction.report.v1.server.service.PlaceholderService.SKIPPED_PLACEHOLDER;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.OffsetDateTime;
@ -71,6 +70,7 @@ import com.iqser.red.service.redaction.report.v1.server.model.ImagePlaceholder;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderInput;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.utils.ImageCache;
import com.iqser.red.service.redaction.report.v1.server.utils.PixelUtil;
import io.micrometer.core.annotation.Timed;
@ -106,6 +106,7 @@ public class ExcelReportGenerationService {
private final ScmReportService componentReportService;
private final FileAttributesConfigClient fileAttributesConfigClient;
private final ComponentRowsReportService componentRowsReportService;
private CreationHelper creationHelper;
@Timed("redactmanager_generateExcelReport")
@ -217,7 +218,6 @@ public class ExcelReportGenerationService {
}
var createdCell = sheet.getRow(indexToAddRow).createCell(cellsToCopyEntry.getKey().getColumnIndex());
createdCell.setCellValue(cellsToCopyEntry.getValue().getStringCellValue());
CellStyle newCellStyle = workbook.createCellStyle();
newCellStyle.cloneStyleFrom(cellsToCopyEntry.getValue().getCellStyle());
@ -274,39 +274,47 @@ public class ExcelReportGenerationService {
for (ImagePlaceholder imagePlaceholder : placeholderModel.getImagePlaceholders()) {
if (cell.getStringCellValue().contains(imagePlaceholder.getPlaceholder())) {
try (ByteArrayInputStream is = new ByteArrayInputStream(imagePlaceholder.getImage())) {
double factor = calculateScale(is,
PixelUtil.widthUnits2Pixel((short) sheet.getColumnWidth(cell.getColumnIndex())),
PixelUtil.heightUnits2Pixel(cell.getRow().getHeight()));
is.reset();
BufferedImage image = ImageCache.getOrLoadImage(imagePlaceholder.getPlaceholder(), imagePlaceholder.getImage());
if (image != null) {
double factor = calculateScale(image,
PixelUtil.widthUnits2Pixel((short) sheet.getColumnWidth(cell.getColumnIndex())),
PixelUtil.heightUnits2Pixel(cell.getRow().getHeight()));
int pictureIdx = workbook.addPicture(is.readAllBytes(), SXSSFWorkbook.PICTURE_TYPE_JPEG);
is.reset();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
baos.flush();
int pictureIdx = workbook.addPicture(baos.toByteArray(), SXSSFWorkbook.PICTURE_TYPE_JPEG);
baos.close();
//Returns an object that handles instantiating concrete classes
CreationHelper helper = workbook.getCreationHelper();
//Create an anchor that is attached to the worksheet
CreationHelper helper = getCreationHelper(workbook);
ClientAnchor anchor = helper.createClientAnchor();
anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
anchor.setCol1(cell.getColumnIndex());
anchor.setRow1(cell.getRowIndex());
//Creates the top-level drawing patriarch.
Drawing<?> drawing = sheet.createDrawingPatriarch();
Picture picture = drawing.createPicture(anchor, pictureIdx);
picture.resize(factor);
cell.setCellValue("");
}
}
}
}
private CreationHelper getCreationHelper(SXSSFWorkbook workbook) {
if (creationHelper == null) {
creationHelper = workbook.getCreationHelper();
}
return creationHelper;
}
private String getPlaceholderValue(String placeholder, String dossierName, String filename, PlaceholderModel placeholderModel) {
if (placeholder.equals(FORMAT_DATE_ISO_PLACEHOLDER)) {
@ -354,12 +362,10 @@ public class ExcelReportGenerationService {
}
private double calculateScale(ByteArrayInputStream imageByteArrayInputStream, float cellWidth, float cellHeight) throws IOException {
private double calculateScale(BufferedImage image, float cellWidth, float cellHeight) {
BufferedImage img = ImageIO.read(imageByteArrayInputStream);
double imageWidth = img.getWidth();
double imageHeight = img.getHeight();
double imageWidth = image.getWidth();
double imageHeight = image.getHeight();
double widthFactor = cellWidth / imageWidth;
double heightFactor = cellHeight / imageHeight;

View File

@ -0,0 +1,101 @@
package com.iqser.red.service.redaction.report.v1.server.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileModel;
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.model.MultiFileWorkbook;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.model.ReportTemplatesModel;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageServiceAsyncWrapper;
import com.iqser.red.service.redaction.report.v1.server.utils.TemplateCache;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@SuppressWarnings("PMD")
public class ExcelReportTemplateService {
ReportStorageService reportStorageService;
ExcelReportGenerationService excelTemplateReportGenerationService;
GeneratePlaceholderService generatePlaceholderService;
ReportStorageServiceAsyncWrapper reportStorageServiceAsyncWrapper;
public CompletableFuture<StoredFileInformation> createExcelReportFromTemplateAsync(Dossier dossier,
FileModel fileStatus,
PlaceholderModel placeholderModel,
String templateName,
String downloadId,
List<ReportRedactionEntry> reportEntries,
ReportTemplate reportTemplate) {
byte[] excelTemplate = TemplateCache.getTemplate(reportTemplate.getStorageId(), reportStorageService);
try (ByteArrayInputStream is = new ByteArrayInputStream(excelTemplate)) {
XSSFWorkbook readWorkbook = new XSSFWorkbook(is);
SXSSFWorkbook writeWorkbook = new SXSSFWorkbook();
for (Sheet sheet : readWorkbook) {
writeWorkbook.createSheet(sheet.getSheetName());
}
var excelModel = excelTemplateReportGenerationService.calculateExcelModel(readWorkbook.getSheetAt(0), dossier.getDossierTemplateId());
if (excelModel.isRssPlaceholdersPresent()) {
generatePlaceholderService.resolveRssValues(fileStatus, placeholderModel);
}
excelTemplateReportGenerationService.generateExcelReport(reportEntries,
placeholderModel,
templateName,
writeWorkbook,
dossier.getDossierName(),
fileStatus,
excelModel,
true);
byte[] template = excelTemplateReportGenerationService.toByteArray(writeWorkbook);
return reportStorageServiceAsyncWrapper.storeObjectAsync(downloadId, template)
.thenApply(storageId -> new StoredFileInformation(fileStatus.getId(), storageId, ReportType.EXCEL_TEMPLATE_SINGLE_FILE, reportTemplate.getTemplateId(), 0));
} catch (IOException e) {
CompletableFuture<StoredFileInformation> finalResultFuture = new CompletableFuture<>();
finalResultFuture.completeExceptionally(new RuntimeException("Could not generate single file excel report.", e));
return finalResultFuture;
}
}
public void prepareExcelReportTemplates(String dossierTemplateId, String templateId, ReportTemplate reportTemplate, ReportTemplatesModel reportTemplatesModel) {
byte[] excelTemplate = TemplateCache.getTemplate(reportTemplate.getStorageId(), reportStorageService);
try (ByteArrayInputStream is = new ByteArrayInputStream(excelTemplate)) {
XSSFWorkbook readWorkbook = new XSSFWorkbook(is);
SXSSFWorkbook writeWorkbook = new SXSSFWorkbook();
for (Sheet sheet : readWorkbook) {
writeWorkbook.createSheet(sheet.getSheetName());
}
MultiFileWorkbook multiFileWorkbook = new MultiFileWorkbook(readWorkbook,
writeWorkbook,
templateId,
reportTemplate.getFileName(),
excelTemplateReportGenerationService.calculateExcelModel(readWorkbook.getSheetAt(0), dossierTemplateId));
reportTemplatesModel.multiFileWorkbookReportTemplates.add(multiFileWorkbook);
} catch (IOException e) {
throw new RuntimeException("Could not generate multifile excel report.");
}
}
}

View File

@ -4,8 +4,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
@ -21,13 +21,15 @@ 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.DossierClient;
import com.iqser.red.service.redaction.report.v1.server.client.FileStatusClient;
import com.iqser.red.service.redaction.report.v1.server.client.ReportTemplateClient;
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;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.model.ReportTemplatesModel;
import com.iqser.red.service.redaction.report.v1.server.settings.ReportTemplateSettings;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageServiceAsyncWrapper;
import com.iqser.red.service.redaction.report.v1.server.utils.TemplateCache;
import io.micrometer.core.annotation.Timed;
import lombok.AccessLevel;
@ -40,39 +42,41 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ReportGenerationService {
private final ReportStorageService reportStorageService;
private final WordReportGenerationService wordReportGenerationService;
private final EntityLogConverterService entityLogConverterService;
private final FileStatusClient fileStatusClient;
private final DossierClient dossierClient;
private final ReportTemplateClient reportTemplateClient;
private final ExcelReportGenerationService excelTemplateReportGenerationService;
private final GeneratePlaceholderService generatePlaceholderService;
private final ReportTemplateSettings reportTemplateSettings;
ReportStorageService reportStorageService;
WordReportGenerationService wordReportGenerationService;
EntityLogConverterService entityLogConverterService;
FileStatusClient fileStatusClient;
DossierClient dossierClient;
ExcelReportGenerationService excelTemplateReportGenerationService;
GeneratePlaceholderService generatePlaceholderService;
ReportTemplateSettings reportTemplateSettings;
ReportStorageServiceAsyncWrapper reportStorageServiceAsyncWrapper;
ReportTemplateService reportTemplateService;
@SneakyThrows
@Timed("redactmanager_generateReports")
public String generateReports(ReportRequestMessage reportMessage) {
List<StoredFileInformation> storedFileInformation = Collections.synchronizedList(new ArrayList<>());
Dossier dossier = dossierClient.getDossierById(reportMessage.getDossierId(), true, false);
ReportTemplates reportTemplates = prepareReportTemplates(reportMessage);
ReportTemplatesModel reportTemplatesModel = reportTemplateService.prepareReportTemplates(reportMessage);
var placeholderModel = generatePlaceholderService.buildPlaceholders(dossier);
String downloadId = reportMessage.getDownloadId();
List<CompletableFuture<Void>> allFutures = new ArrayList<>();
for (int fileIdIndex = 0; fileIdIndex < reportMessage.getFileIds().size(); fileIdIndex++) {
long start = System.currentTimeMillis();
var isLastFile = fileIdIndex == reportMessage.getFileIds().size() - 1;
String dossierId = dossier.getId();
String fileId = reportMessage.getFileIds().get(fileIdIndex);
String fileId = reportMessage.getFileIds()
.get(fileIdIndex);
String dossierName = dossier.getDossierName();
var fileStatus = fileStatusClient.getFileStatus(dossierId, fileId);
@ -80,88 +84,57 @@ public class ReportGenerationService {
List<ReportRedactionEntry> reportEntries = entityLogConverterService.getReportEntries(dossierId, fileId, fileStatus.isExcluded());
generateMultiFileExcelReports(reportTemplates.multiFileWorkbookReportTemplates, placeholderModel, fileStatus, isLastFile, dossierName, reportEntries);
generateMultiFileExcelReports(reportTemplatesModel.multiFileWorkbookReportTemplates, placeholderModel, fileStatus, isLastFile, dossierName, reportEntries);
generateMultiFileWordReports(reportTemplates.multiFileDocumentReportTemplates,
storedFileInformation,
dossier,
placeholderModel,
isLastFile,
downloadId,
fileStatus,
reportEntries);
CompletableFuture<Void> multiFileWordReportsFuture = generateMultiFileWordReportsAsync(reportTemplatesModel.multiFileDocumentReportTemplates,
storedFileInformation,
dossier,
placeholderModel,
isLastFile,
downloadId,
fileStatus,
reportEntries);
allFutures.add(multiFileWordReportsFuture);
generateSingleFileReports(reportTemplates.singleFilesTemplates, storedFileInformation, dossier, placeholderModel, downloadId, fileStatus, reportEntries);
CompletableFuture<Void> singleFileReportsAsync = generateSingleFileReportsAsync(reportTemplatesModel.singleFilesTemplates,
storedFileInformation,
dossier,
placeholderModel,
downloadId,
fileStatus,
reportEntries);
allFutures.add(singleFileReportsAsync);
long end = System.currentTimeMillis();
log.info("Successfully processed {}/{} fileIds for downloadId {}, took {}", fileIdIndex + 1, reportMessage.getFileIds().size(), downloadId, end - start);
}
for (MultiFileWorkbook multiFileWorkbook : reportTemplates.multiFileWorkbookReportTemplates) {
for (MultiFileWorkbook multiFileWorkbook : reportTemplatesModel.multiFileWorkbookReportTemplates) {
byte[] template = excelTemplateReportGenerationService.toByteArray(multiFileWorkbook.getWriteWorkbook());
String storageId = reportStorageService.storeObject(downloadId, template);
storedFileInformation.add(new StoredFileInformation(null, storageId, ReportType.EXCEL_TEMPLATE_MULTI_FILE, multiFileWorkbook.getTemplateId(), 0));
CompletableFuture<Void> future = reportStorageServiceAsyncWrapper.storeObjectAsync(downloadId, template).thenAccept(storageId -> {
storedFileInformation.add(new StoredFileInformation(null, storageId, ReportType.EXCEL_TEMPLATE_MULTI_FILE, multiFileWorkbook.getTemplateId(), 0));
});
allFutures.add(future);
}
for (MultiFileDocument multiFileDocument : reportTemplates.multiFileDocumentReportTemplates) {
for (MultiFileDocument multiFileDocument : reportTemplatesModel.multiFileDocumentReportTemplates) {
byte[] template = wordReportGenerationService.toByteArray(multiFileDocument.getDocument());
String storageId = reportStorageService.storeObject(downloadId, template);
storedFileInformation.add(new StoredFileInformation(null,
storageId,
ReportType.WORD_TEMPLATE_MULTI_FILE,
multiFileDocument.getTemplateId(),
multiFileDocument.getDocumentPartNr()));
CompletableFuture<Void> future = reportStorageServiceAsyncWrapper.storeObjectAsync(downloadId, template).thenAccept(storageId -> {
storedFileInformation.add(new StoredFileInformation(null,
storageId,
ReportType.WORD_TEMPLATE_MULTI_FILE,
multiFileDocument.getTemplateId(),
multiFileDocument.getDocumentPartNr()));
});
allFutures.add(future);
}
CompletableFuture.allOf(allFutures.toArray(new CompletableFuture[0])).join();
return reportStorageService.storeReportInformation(reportMessage.getDownloadId(), storedFileInformation);
}
private ReportTemplates prepareReportTemplates(ReportRequestMessage reportMessage) {
var reportTemplates = new ReportTemplates();
for (String templateId : reportMessage.getTemplateIds()) {
try {
ReportTemplate reportTemplate = reportTemplateClient.getReportTemplate(reportMessage.getDossierTemplateId(), templateId);
if (reportTemplate.isMultiFileReport()) {
if (reportTemplate.getFileName().endsWith(".xlsx")) {
byte[] excelTemplate = reportStorageService.getReportTemplate(reportTemplate.getStorageId());
try (ByteArrayInputStream is = new ByteArrayInputStream(excelTemplate)) {
XSSFWorkbook readWorkbook = new XSSFWorkbook(is);
SXSSFWorkbook writeWorkbook = new SXSSFWorkbook();
for (Sheet sheet : readWorkbook) {
writeWorkbook.createSheet(sheet.getSheetName());
}
MultiFileWorkbook multiFileWorkbook = new MultiFileWorkbook(readWorkbook,
writeWorkbook,
templateId,
reportTemplate.getFileName(),
excelTemplateReportGenerationService.calculateExcelModel(readWorkbook.getSheetAt(0), reportMessage.getDossierTemplateId()));
reportTemplates.multiFileWorkbookReportTemplates.add(multiFileWorkbook);
} catch (IOException e) {
throw new RuntimeException("Could not generate multifile excel report.");
}
} else {
byte[] wordTemplate = reportStorageService.getReportTemplate(reportTemplate.getStorageId());
try (ByteArrayInputStream is = new ByteArrayInputStream(wordTemplate)) {
XWPFDocument doc = new XWPFDocument(is);
MultiFileDocument multiFileDocument = new MultiFileDocument(wordTemplate, doc, templateId, reportTemplate.getFileName(), 0, 0);
reportTemplates.multiFileDocumentReportTemplates.add(multiFileDocument);
} catch (IOException e) {
throw new RuntimeException("Could not generate multifile word report.");
}
}
} else {
reportTemplates.singleFilesTemplates.add(reportTemplate);
}
} catch (Exception e) {
log.warn("Skipping reportTemplate with id {}", templateId);
}
}
return reportTemplates;
}
private void generateMultiFileExcelReports(List<MultiFileWorkbook> multiFileWorkbookReportTemplates,
PlaceholderModel placeholderModel,
FileModel fileStatus,
@ -174,130 +147,88 @@ public class ReportGenerationService {
generatePlaceholderService.resolveRssValues(fileStatus, placeholderModel);
}
excelTemplateReportGenerationService.generateExcelReport(reportEntries,
placeholderModel,
multiFileWorkbook.getTemplateName(),
multiFileWorkbook.getWriteWorkbook(),
dossierName,
fileStatus,
multiFileWorkbook.getExcelModel(),
isLastFile);
placeholderModel,
multiFileWorkbook.getTemplateName(),
multiFileWorkbook.getWriteWorkbook(),
dossierName,
fileStatus,
multiFileWorkbook.getExcelModel(),
isLastFile);
}
}
private void generateMultiFileWordReports(List<MultiFileDocument> multiFileDocumentReportTemplates,
List<StoredFileInformation> storedFileInformation,
Dossier dossier,
PlaceholderModel placeholderModel,
boolean isLastFile,
String downloadId,
FileModel fileStatus,
List<ReportRedactionEntry> reportEntries) throws IOException {
private CompletableFuture<Void> generateMultiFileWordReportsAsync(List<MultiFileDocument> multiFileDocumentReportTemplates,
List<StoredFileInformation> storedFileInformation,
Dossier dossier,
PlaceholderModel placeholderModel,
boolean isLastFile,
String downloadId,
FileModel fileStatus,
List<ReportRedactionEntry> reportEntries) {
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (MultiFileDocument multiFileDocument : multiFileDocumentReportTemplates) {
var numberOfChars = wordReportGenerationService.approxNumberOfChars(reportEntries.stream().findFirst(), fileStatus.getFilename());
var numberOfChars = wordReportGenerationService.approxNumberOfChars(reportEntries.stream()
.findFirst(), fileStatus.getFilename());
if (multiFileDocument.getNumberOfChars() >= reportTemplateSettings.getMultiFileChunkSize()) {
wordReportGenerationService.removePlaceholdersRow(wordReportGenerationService.getRedactionTable(multiFileDocument.getDocument()));
byte[] wordDoc = wordReportGenerationService.toByteArray(multiFileDocument.getDocument());
String storageId = reportStorageService.storeObject(downloadId, wordDoc);
storedFileInformation.add(new StoredFileInformation(null,
storageId,
ReportType.WORD_TEMPLATE_MULTI_FILE,
multiFileDocument.getTemplateId(),
multiFileDocument.getDocumentPartNr()));
multiFileDocument.setDocumentPartNr(multiFileDocument.getDocumentPartNr() + 1);
try (ByteArrayInputStream is = new ByteArrayInputStream(multiFileDocument.getTemplateAsBytes())) {
XWPFDocument doc = new XWPFDocument(is);
multiFileDocument.setDocument(doc);
multiFileDocument.setNumberOfChars(0);
}
CompletableFuture<Void> future = reportStorageServiceAsyncWrapper.storeObjectAsync(downloadId, wordDoc).thenAccept(storageId -> {
storedFileInformation.add(new StoredFileInformation(null,
storageId,
ReportType.WORD_TEMPLATE_MULTI_FILE,
multiFileDocument.getTemplateId(),
multiFileDocument.getDocumentPartNr()));
multiFileDocument.setDocumentPartNr(multiFileDocument.getDocumentPartNr() + 1);
try (ByteArrayInputStream is = new ByteArrayInputStream(multiFileDocument.getTemplateAsBytes())) {
XWPFDocument doc = new XWPFDocument(is);
multiFileDocument.setDocument(doc);
multiFileDocument.setNumberOfChars(0);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
futures.add(future);
}
numberOfChars = wordReportGenerationService.generateWordReport(reportEntries,
placeholderModel,
multiFileDocument.getTemplateName(),
multiFileDocument.getDocument(),
fileStatus,
dossier,
isLastFile ? isLastFile : multiFileDocument.getNumberOfChars() + numberOfChars >= reportTemplateSettings.getMultiFileChunkSize());
placeholderModel,
multiFileDocument.getTemplateName(),
multiFileDocument.getDocument(),
fileStatus,
dossier,
isLastFile ? isLastFile : multiFileDocument.getNumberOfChars() + numberOfChars
>= reportTemplateSettings.getMultiFileChunkSize());
multiFileDocument.setNumberOfChars(multiFileDocument.getNumberOfChars() + numberOfChars);
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
private void generateSingleFileReports(List<ReportTemplate> singleFilesTemplates,
List<StoredFileInformation> storedFileInformation,
Dossier dossier,
PlaceholderModel placeholderModel,
String downloadId,
FileModel fileStatus,
List<ReportRedactionEntry> reportEntries) {
for (ReportTemplate reportTemplate : singleFilesTemplates) {
storedFileInformation.add(createReportFromTemplate(dossier, fileStatus, placeholderModel, reportTemplate.getFileName(), downloadId, reportEntries, reportTemplate));
}
}
private StoredFileInformation createReportFromTemplate(Dossier dossier,
FileModel fileStatus,
PlaceholderModel placeholderModel,
String templateName,
String downloadId,
List<ReportRedactionEntry> reportEntries,
ReportTemplate reportTemplate) {
if (reportTemplate.getFileName().endsWith(".xlsx")) {
byte[] excelTemplate = reportStorageService.getReportTemplate(reportTemplate.getStorageId());
try (ByteArrayInputStream is = new ByteArrayInputStream(excelTemplate)) {
XSSFWorkbook readWorkbook = new XSSFWorkbook(is);
SXSSFWorkbook writeWorkbook = new SXSSFWorkbook();
for (Sheet sheet : readWorkbook) {
writeWorkbook.createSheet(sheet.getSheetName());
}
var excelModel = excelTemplateReportGenerationService.calculateExcelModel(readWorkbook.getSheetAt(0), dossier.getDossierTemplateId());
if (excelModel.isRssPlaceholdersPresent()) {
generatePlaceholderService.resolveRssValues(fileStatus, placeholderModel);
}
excelTemplateReportGenerationService.generateExcelReport(reportEntries,
placeholderModel,
templateName,
writeWorkbook,
dossier.getDossierName(),
fileStatus,
excelModel,
true);
byte[] template = excelTemplateReportGenerationService.toByteArray(writeWorkbook);
String storageId = reportStorageService.storeObject(downloadId, template);
return new StoredFileInformation(fileStatus.getId(), storageId, ReportType.EXCEL_TEMPLATE_SINGLE_FILE, reportTemplate.getTemplateId(), 0);
} catch (IOException e) {
throw new RuntimeException("Could not generate singlefile excel report.");
}
} else {
byte[] wordTemplate = reportStorageService.getReportTemplate(reportTemplate.getStorageId());
try (ByteArrayInputStream is = new ByteArrayInputStream(wordTemplate)) {
XWPFDocument doc = new XWPFDocument(is);
wordReportGenerationService.generateWordReport(reportEntries, placeholderModel, templateName, doc, fileStatus, dossier, true);
byte[] template = wordReportGenerationService.toByteArray(doc);
String storageId = reportStorageService.storeObject(downloadId, template);
return new StoredFileInformation(fileStatus.getId(), storageId, ReportType.WORD_SINGLE_FILE, reportTemplate.getTemplateId(), 0);
} catch (IOException e) {
throw new RuntimeException("Could not generate singlefile word report.");
}
}
}
@FieldDefaults(makeFinal = true, level = AccessLevel.PUBLIC)
private static final class ReportTemplates {
List<ReportTemplate> singleFilesTemplates = new LinkedList<>();
List<MultiFileWorkbook> multiFileWorkbookReportTemplates = new LinkedList<>();
List<MultiFileDocument> multiFileDocumentReportTemplates = new LinkedList<>();
private CompletableFuture<Void> generateSingleFileReportsAsync(List<ReportTemplate> singleFilesTemplates,
List<StoredFileInformation> storedFileInformation,
Dossier dossier,
PlaceholderModel placeholderModel,
String downloadId,
FileModel fileStatus,
List<ReportRedactionEntry> reportEntries) {
return CompletableFuture.allOf(singleFilesTemplates.stream()
.map(reportTemplate -> reportTemplateService.createReportFromTemplateAsync(dossier,
fileStatus,
placeholderModel,
reportTemplate.getFileName(),
downloadId,
reportEntries,
reportTemplate).thenAccept(storedFileInformation::add))
.toArray(CompletableFuture[]::new));
}
}

View File

@ -0,0 +1,76 @@
package com.iqser.red.service.redaction.report.v1.server.service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.shared.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.ReportTemplateClient;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.model.ReportTemplatesModel;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ReportTemplateService {
static String XLSX_EXTENSION = ".xlsx";
ReportTemplateClient reportTemplateClient;
WordReportTemplateService wordReportTemplateService;
ExcelReportTemplateService excelReportTemplateService;
public ReportTemplatesModel prepareReportTemplates(ReportRequestMessage reportMessage) {
ReportTemplatesModel reportTemplates = new ReportTemplatesModel();
for (String templateId : reportMessage.getTemplateIds()) {
try {
String dossierTemplatedId = reportMessage.getDossierTemplateId();
ReportTemplate reportTemplate = reportTemplateClient.getReportTemplate(dossierTemplatedId, templateId);
if (reportTemplate.isMultiFileReport()) {
if (reportTemplate.getFileName().endsWith(XLSX_EXTENSION)) {
excelReportTemplateService.prepareExcelReportTemplates(dossierTemplatedId, templateId, reportTemplate, reportTemplates);
} else {
wordReportTemplateService.prepareWordReportTemplates(templateId, reportTemplate, reportTemplates);
}
} else {
reportTemplates.singleFilesTemplates.add(reportTemplate);
}
} catch (Exception e) {
log.warn("Skipping reportTemplate with id {}, exception {}", templateId, e.getMessage());
}
}
return reportTemplates;
}
public CompletableFuture<StoredFileInformation> createReportFromTemplateAsync(Dossier dossier,
FileModel fileStatus,
PlaceholderModel placeholderModel,
String templateName,
String downloadId,
List<ReportRedactionEntry> reportEntries,
ReportTemplate reportTemplate) {
if (reportTemplate.getFileName().endsWith(XLSX_EXTENSION)) {
return excelReportTemplateService.createExcelReportFromTemplateAsync(dossier, fileStatus, placeholderModel, templateName, downloadId, reportEntries, reportTemplate);
} else {
return wordReportTemplateService.createWordReportFromTemplateAsync(dossier, fileStatus, placeholderModel, templateName, downloadId, reportEntries, reportTemplate);
}
}
}

View File

@ -68,6 +68,7 @@ import com.iqser.red.service.redaction.report.v1.server.model.PlaceHolderFunctio
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderInput;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.utils.ImageCache;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
@ -151,11 +152,13 @@ public class WordReportGenerationService {
private void replaceImagePlaceholders(XWPFDocument doc, ImagePlaceholder imagePlaceholder) {
replaceParagraphForImagePlaceholder(doc.getParagraphs(), imagePlaceholder);
for (XWPFTable tbl : doc.getTables()) {
for (XWPFTableRow row : tbl.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
replaceParagraphForImagePlaceholder(cell.getParagraphs(), imagePlaceholder);
BufferedImage img = ImageCache.getOrLoadImage(imagePlaceholder.getPlaceholder(), imagePlaceholder.getImage());
if (img != null) {
for (XWPFTable tbl : doc.getTables()) {
for (XWPFTableRow row : tbl.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
replaceParagraphForImagePlaceholder(cell.getParagraphs(), imagePlaceholder, img);
}
}
}
}
@ -202,20 +205,24 @@ public class WordReportGenerationService {
}
private void replaceParagraphForImagePlaceholder(List<XWPFParagraph> paragraphs, ImagePlaceholder imagePlaceholder) {
private void replaceParagraphForImagePlaceholder(List<XWPFParagraph> paragraphs, ImagePlaceholder imagePlaceholder, BufferedImage img) {
for (XWPFParagraph p : paragraphs) {
String paragraphText = p.getText();
if (paragraphText.contains(imagePlaceholder.getPlaceholder())) {
try (ByteArrayInputStream is2 = new ByteArrayInputStream(imagePlaceholder.getImage())) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "jpg", baos);
baos.flush();
ByteArrayInputStream is = new ByteArrayInputStream(baos.toByteArray());
XWPFRun run = p.getRuns().get(0);
run.setText("", 0);
run.addBreak();
Dimension2DDouble dim = getImageDimension(is2);
run.addPicture(is2, XWPFDocument.PICTURE_TYPE_JPEG, "image.jpg", Units.toEMU(dim.getWidth()), Units.toEMU(dim.getHeight()));
int size = p.getRuns().size();
for (int i = 1; i < size; i++) {
p.removeRun(1);
run.addPicture(is, XWPFDocument.PICTURE_TYPE_JPEG, "image.jpg", Units.toEMU(img.getWidth()), Units.toEMU(img.getHeight()));
for (int i = p.getRuns().size() - 1; i > 0; i--) {
p.removeRun(i);
}
} catch (IOException | InvalidFormatException e) {
throw new RuntimeException(e);

View File

@ -0,0 +1,72 @@
package com.iqser.red.service.redaction.report.v1.server.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileModel;
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.model.MultiFileDocument;
import com.iqser.red.service.redaction.report.v1.server.model.PlaceholderModel;
import com.iqser.red.service.redaction.report.v1.server.model.ReportRedactionEntry;
import com.iqser.red.service.redaction.report.v1.server.model.ReportTemplatesModel;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageServiceAsyncWrapper;
import com.iqser.red.service.redaction.report.v1.server.utils.TemplateCache;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
@Service
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@SuppressWarnings("PMD")
public class WordReportTemplateService {
ReportStorageService reportStorageService;
ReportStorageServiceAsyncWrapper reportStorageServiceAsyncWrapper;
WordReportGenerationService wordReportGenerationService;
public CompletableFuture<StoredFileInformation> createWordReportFromTemplateAsync(Dossier dossier,
FileModel fileStatus,
PlaceholderModel placeholderModel,
String templateName,
String downloadId,
List<ReportRedactionEntry> reportEntries,
ReportTemplate reportTemplate) {
byte[] wordTemplate = TemplateCache.getTemplate(reportTemplate.getStorageId(), reportStorageService);
try (ByteArrayInputStream is = new ByteArrayInputStream(wordTemplate)) {
XWPFDocument doc = new XWPFDocument(is);
wordReportGenerationService.generateWordReport(reportEntries, placeholderModel, templateName, doc, fileStatus, dossier, true);
byte[] template = wordReportGenerationService.toByteArray(doc);
return reportStorageServiceAsyncWrapper.storeObjectAsync(downloadId, template)
.thenApply(storageId -> new StoredFileInformation(fileStatus.getId(), storageId, ReportType.WORD_SINGLE_FILE, reportTemplate.getTemplateId(), 0));
} catch (IOException e) {
CompletableFuture<StoredFileInformation> finalResultFuture = new CompletableFuture<>();
finalResultFuture.completeExceptionally(new RuntimeException("Could not generate single file word report.", e));
return finalResultFuture;
}
}
public void prepareWordReportTemplates(String templateId, ReportTemplate reportTemplate, ReportTemplatesModel reportTemplatesModel) {
byte[] wordTemplate = TemplateCache.getTemplate(reportTemplate.getStorageId(), reportStorageService);
try (ByteArrayInputStream is = new ByteArrayInputStream(wordTemplate)) {
XWPFDocument doc = new XWPFDocument(is);
MultiFileDocument multiFileDocument = new MultiFileDocument(wordTemplate, doc, templateId, reportTemplate.getFileName(), 0, 0);
reportTemplatesModel.multiFileDocumentReportTemplates.add(multiFileDocument);
} catch (IOException e) {
throw new RuntimeException("Could not generate multifile word report.");
}
}
}

View File

@ -0,0 +1,25 @@
package com.iqser.red.service.redaction.report.v1.server.storage;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
/**
* This is an async wrapper over our storage implementation so that we can store the files for the report asynchronously.
*/
@RequiredArgsConstructor
@Service
public class ReportStorageServiceAsyncWrapper {
private final ReportStorageService reportStorageService;
private final Executor ioExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public CompletableFuture<String> storeObjectAsync(String downloadId, byte[] data) {
return CompletableFuture.supplyAsync(() -> reportStorageService.storeObject(downloadId, data), ioExecutor);
}
}

View File

@ -0,0 +1,42 @@
package com.iqser.red.service.redaction.report.v1.server.utils;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import javax.imageio.ImageIO;
import lombok.extern.slf4j.Slf4j;
/**
* This is a caching mechanism for images, to avoid loading images with ImageIO.read() inside a loop which is inefficient,
* especially if we load the same image multiple times.
*/
@Slf4j
public class ImageCache {
private static final Cache<String, BufferedImage> cache = Caffeine.newBuilder().maximumSize(100)
.build();
public static BufferedImage getOrLoadImage(String imageKey, byte[] imageData) {
return cache.get(imageKey, key -> loadImageFromBytes(imageData));
}
private static BufferedImage loadImageFromBytes(byte[] imageData) {
try (ByteArrayInputStream is = new ByteArrayInputStream(imageData)) {
return ImageIO.read(is);
} catch (IOException e) {
log.error("Error loading image: ", e);
return null;
}
}
}

View File

@ -0,0 +1,31 @@
package com.iqser.red.service.redaction.report.v1.server.utils;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.iqser.red.service.redaction.report.v1.server.storage.ReportStorageService;
import lombok.extern.slf4j.Slf4j;
/**
* This is a caching mechanism for templates to avoid getting a template from storage for each file when we request a download,
* since the templates are provided at startup.
*/
@Slf4j
public class TemplateCache {
private static final Cache<String, byte[]> cache = Caffeine.newBuilder().maximumSize(500).build();
public static byte[] getTemplate(String templateId, ReportStorageService reportStorageService) {
return cache.get(templateId, reportStorageService::getReportTemplate);
}
public static void evictCache(String templateId) {
log.info("Evicting cache for template {}", templateId);
cache.invalidate(templateId);
}
}