From 3e9e9e1d347aad98dc4b4d68a5e4da91296859ce Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 10 Nov 2025 22:00:31 +0100 Subject: [PATCH] Replaced MaterialExcelMapper with MaterialFastExcelMapper using fastexcel instead of Apache POI. --- pom.xml | 10 + .../src/components/UI/AppListItem.vue | 2 +- .../layout/error/ErrorModalOverview.vue | 4 +- .../lcc/service/bulk/BulkExportService.java | 27 +- .../lcc/service/bulk/BulkImportService.java | 30 ++- .../bulk/BulkOperationExecutionService.java | 34 ++- .../excelMapper/MaterialFastExcelMapper.java | 254 ++++++++++++++++++ .../nodes/NodeUpdateDTOTransformer.java | 3 +- 8 files changed, 330 insertions(+), 34 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/service/excelMapper/MaterialFastExcelMapper.java diff --git a/pom.xml b/pom.xml index 8f633db..94a45e6 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,16 @@ poi 5.4.1 + + org.dhatim + fastexcel + 0.17.0 + + + org.dhatim + fastexcel-reader + 0.17.0 + org.apache.poi poi-ooxml diff --git a/src/frontend/src/components/UI/AppListItem.vue b/src/frontend/src/components/UI/AppListItem.vue index a48c8f7..fc022a2 100644 --- a/src/frontend/src/components/UI/AppListItem.vue +++ b/src/frontend/src/components/UI/AppListItem.vue @@ -43,7 +43,7 @@ export default { display: grid; grid-template-columns: 1fr 2fr 0.5fr; grid-gap: 2.4rem; - padding: 1.6rem 0; + padding: 1.2rem 0; font-weight: 400; font-size: 1.4rem; border-bottom: 0.1rem solid #E3EDFF; diff --git a/src/frontend/src/components/layout/error/ErrorModalOverview.vue b/src/frontend/src/components/layout/error/ErrorModalOverview.vue index a296852..df076e3 100644 --- a/src/frontend/src/components/layout/error/ErrorModalOverview.vue +++ b/src/frontend/src/components/layout/error/ErrorModalOverview.vue @@ -109,9 +109,9 @@ export default { await navigator.clipboard.writeText(JSON.stringify(this.error)); this.$refs.toast.addToast({ - icon: 'Bug', + icon: 'Clipboard', message: "The error has been copied to clipboard", - title: "Copied to clipboard", + title: "Successful copied to clipboard", variant: 'success', duration: 4000 }) diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java index b437cce..6baf9f7 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java @@ -30,8 +30,9 @@ public class BulkExportService { private final HiddenNodeExcelMapper hiddenNodeExcelMapper; private final HiddenCountryExcelMapper hiddenCountryExcelMapper; private final String sheetPassword; + private final MaterialFastExcelMapper materialFastExcelMapper; - public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper) { + public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) { this.headerCellStyleProvider = headerCellStyleProvider; this.containerRateExcelMapper = containerRateExcelMapper; this.matrixRateExcelMapper = matrixRateExcelMapper; @@ -41,10 +42,24 @@ public class BulkExportService { this.hiddenNodeExcelMapper = hiddenNodeExcelMapper; this.hiddenCountryExcelMapper = hiddenCountryExcelMapper; this.sheetPassword = sheetPassword; + this.materialFastExcelMapper = materialFastExcelMapper; + } + + public void processOperation(BulkOperation op) throws IOException { + var bulkFileType = op.getFileType(); + + // Use FastExcel for MATERIAL, Apache POI for others + if (bulkFileType.equals(BulkFileType.MATERIAL)) { + byte[] excelData = materialFastExcelMapper.exportToExcel(); + op.setFile(excelData); + } else { + processOperationWithApachePOI(op); + } } - public void processOperation(BulkOperation op) throws IOException { + + public void processOperationWithApachePOI(BulkOperation op) throws IOException { var bulkFileType = op.getFileType(); var periodId = op.getValidityPeriodId(); @@ -78,10 +93,10 @@ public class BulkExportService { matrixRateExcelMapper.fillSheet(worksheet, style, periodId); matrixRateExcelMapper.createConstraints(workbook, worksheet); break; - case MATERIAL: - materialExcelMapper.fillSheet(worksheet, style); - materialExcelMapper.createConstraints(worksheet); - break; +// case MATERIAL: +// materialExcelMapper.fillSheet(worksheet, style); +// materialExcelMapper.createConstraints(worksheet); +// break; case PACKAGING: packagingExcelMapper.fillSheet(worksheet, style); packagingExcelMapper.createConstraints(workbook, worksheet); diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java index 9acb10b..aa77891 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java @@ -1,5 +1,6 @@ package de.avatic.lcc.service.bulk; +import de.avatic.lcc.dto.bulk.BulkFileType; import de.avatic.lcc.model.bulk.BulkFileTypes; import de.avatic.lcc.model.bulk.BulkOperation; import de.avatic.lcc.service.api.BatchGeoApiService; @@ -31,8 +32,9 @@ public class BulkImportService { private final MatrixRateImportService matrixRateImportService; private final ContainerRateImportService containerRateImportService; private final BatchGeoApiService batchGeoApiService; + private final MaterialFastExcelMapper materialFastExcelMapper; - public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService) { + public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService, MaterialFastExcelMapper materialFastExcelMapper) { this.matrixRateExcelMapper = matrixRateExcelMapper; this.containerRateExcelMapper = containerRateExcelMapper; this.materialExcelMapper = materialExcelMapper; @@ -44,9 +46,27 @@ public class BulkImportService { this.matrixRateImportService = matrixRateImportService; this.containerRateImportService = containerRateImportService; this.batchGeoApiService = batchGeoApiService; + this.materialFastExcelMapper = materialFastExcelMapper; } public void processOperation(BulkOperation op) throws IOException { + var file = op.getFile(); + var type = op.getFileType(); + + // Use FastExcel for MATERIAL, Apache POI for others + if (type.equals(BulkFileType.MATERIAL)) { + processOperationWithFastExcel(op); + } else { + processOperationWithApachePOI(op); + } + } + + private void processOperationWithFastExcel(BulkOperation op) throws IOException { + var materials = materialFastExcelMapper.importFromExcel(op.getFile()); + materials.forEach(materialBulkImportService::processMaterialInstructions); + } + + private void processOperationWithApachePOI(BulkOperation op) throws IOException { var file = op.getFile(); var type = op.getFileType(); @@ -76,10 +96,10 @@ public class BulkImportService { var matrixRates = matrixRateExcelMapper.extractSheet(sheet); matrixRateImportService.processMatrixRates(matrixRates); break; - case MATERIAL: - var materials = materialExcelMapper.extractSheet(sheet); - materials.forEach(materialBulkImportService::processMaterialInstructions); - break; +// case MATERIAL: +// var materials = materialExcelMapper.extractSheet(sheet); +// materials.forEach(materialBulkImportService::processMaterialInstructions); +// break; case PACKAGING: var packaging = packagingExcelMapper.extractSheet(sheet); packaging.forEach(packagingBulkImportService::processPackagingInstructions); diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkOperationExecutionService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkOperationExecutionService.java index 81d62f1..8c1ecb0 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkOperationExecutionService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkOperationExecutionService.java @@ -37,27 +37,25 @@ public class BulkOperationExecutionService { @Async("bulkProcessingExecutor") public CompletableFuture launchExecution(Integer id) { - return CompletableFuture.runAsync(() -> { - execution(id); - }, bulkProcessingExecutor) - .orTimeout(30, TimeUnit.MINUTES) - .exceptionally(e -> { - bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION); + try { + execution(id); + return CompletableFuture.completedFuture(null); + } catch (Exception e) { + bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION); + var error = new SysError(); + error.setType(SysErrorType.BULK); + error.setCode(e.getClass().getSimpleName()); + error.setTitle("Bulk operation execution id" + id + " failed"); + error.setMessage(e.getMessage() == null ? "" : e.getMessage()); + error.setUserId(null); + error.setBulkOperationId(id); + error.setTrace(Arrays.stream(e.getStackTrace()).map(sysErrorTransformer::toSysErrorTraceItem).toList()); - var error = new SysError(); - error.setType(SysErrorType.BULK); - error.setCode(e.getClass().getSimpleName()); - error.setTitle("Bulk operation execution id" + id + " failed with timeout"); - error.setMessage(e.getMessage() == null ? "" : e.getMessage()); - error.setUserId(null); - error.setBulkOperationId(id); - error.setTrace(Arrays.stream(e.getStackTrace()).map(sysErrorTransformer::toSysErrorTraceItem).toList()); + sysErrorRepository.insert(error); - sysErrorRepository.insert(error); - - return null; - }); + return CompletableFuture.failedFuture(e); + } } public void execution(Integer id) { diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/MaterialFastExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/MaterialFastExcelMapper.java new file mode 100644 index 0000000..fbf1440 --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/excelMapper/MaterialFastExcelMapper.java @@ -0,0 +1,254 @@ +package de.avatic.lcc.service.excelMapper; + +import de.avatic.lcc.model.bulk.BulkInstruction; +import de.avatic.lcc.model.bulk.BulkInstructionType; +import de.avatic.lcc.model.bulk.header.MaterialHeader; +import de.avatic.lcc.model.db.materials.Material; +import de.avatic.lcc.repositories.MaterialRepository; +import de.avatic.lcc.util.exception.internalerror.ExcelValidationError; +import org.dhatim.fastexcel.Workbook; +import org.dhatim.fastexcel.Worksheet; +import org.dhatim.fastexcel.reader.ReadableWorkbook; +import org.dhatim.fastexcel.reader.Row; +import org.dhatim.fastexcel.reader.Sheet; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MaterialFastExcelMapper { + + private final MaterialRepository materialRepository; + + public MaterialFastExcelMapper(MaterialRepository materialRepository) { + this.materialRepository = materialRepository; + } + + /** + * Exports materials to Excel using FastExcel + */ + public byte[] exportToExcel() throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + try (Workbook workbook = new Workbook(outputStream, "LCC", "1.0")) { + Worksheet worksheet = workbook.newWorksheet("Materials"); + + // Create header row + createHeaderRow(worksheet); + + // Fill data rows + List materials = materialRepository.listAllMaterials(); + int rowIndex = 1; + for (Material material : materials) { + mapToRow(material, worksheet, rowIndex); + rowIndex++; + } + + // Auto-size columns + for (int i = 0; i < MaterialHeader.values().length; i++) { + worksheet.width(i, 20); + } + + workbook.finish(); + } + + return outputStream.toByteArray(); + } + + /** + * Creates the header row with proper formatting matching Apache POI style + */ + private void createHeaderRow(Worksheet worksheet) { + int colIndex = 0; + for (MaterialHeader header : MaterialHeader.values()) { + worksheet.value(0, colIndex, header.name()); + + // Apply styling to match HeaderCellStyleProvider + worksheet.style(0, colIndex) + .bold() + .fontName("Arial") + .fontSize(10) + .fontColor("002F54") // RGB(0, 47, 84) - Blue text + .fillColor("5AF0B4") // RGB(90, 240, 180) - Green background + .horizontalAlignment("center") + .borderStyle("thin") // All borders thin + .set(); + + colIndex++; + } + } + + /** + * Maps a Material entity to a row in the worksheet + */ + private void mapToRow(Material material, Worksheet worksheet, int rowIndex) { + worksheet.value(rowIndex, MaterialHeader.OPERATION.ordinal(), BulkInstructionType.UPDATE.name()); + worksheet.value(rowIndex, MaterialHeader.PART_NUMBER.ordinal(), material.getPartNumber()); + worksheet.value(rowIndex, MaterialHeader.DESCRIPTION.ordinal(), material.getName()); + worksheet.value(rowIndex, MaterialHeader.HS_CODE.ordinal(), material.getHsCode()); + } + + /** + * Imports materials from Excel using FastExcel + */ + public List> importFromExcel(byte[] fileData) throws IOException { + List> materials = new ArrayList<>(); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(fileData); + ReadableWorkbook workbook = new ReadableWorkbook(inputStream)) { + + Sheet sheet = workbook.getFirstSheet(); + + // Validate header + validateHeader(sheet); + + // Process data rows (skip header row at index 0) + List rows = sheet.read(); + for (int i = 1; i < rows.size(); i++) { + Row row = rows.get(i); + if (!isEmpty(row)) { + materials.add(mapToEntity(row, i + 1)); + } + } + } + + return materials; + } + + /** + * Validates that the Excel file has the correct header structure + */ + private void validateHeader(Sheet sheet) throws IOException { + List rows = sheet.read(); + if (rows.isEmpty()) { + throw new ExcelValidationError("Excel file is empty"); + } + + Row headerRow = rows.get(0); + List expectedHeaders = Arrays.stream(MaterialHeader.values()) + .map(Enum::name) + .collect(Collectors.toList()); + + for (int i = 0; i < expectedHeaders.size(); i++) { + String cellValue = headerRow.getCellAsString(i).orElse(""); + if (!expectedHeaders.get(i).equals(cellValue)) { + throw new ExcelValidationError( + String.format("Invalid header at column %d. Expected '%s' but found '%s'", + i, expectedHeaders.get(i), cellValue) + ); + } + } + } + + /** + * Checks if a row is empty + */ + private boolean isEmpty(Row row) { + for (int i = 0; i < MaterialHeader.values().length; i++) { + if (row.getCellAsString(i).isPresent() && !row.getCellAsString(i).get().trim().isEmpty()) { + return false; + } + } + return true; + } + + /** + * Maps a row from Excel to a BulkInstruction + */ + private BulkInstruction mapToEntity(Row row, int rowNumber) { + Material entity = new Material(); + + try { + // Extract and validate data + String partNumber = getCellValue(row, MaterialHeader.PART_NUMBER.ordinal(), rowNumber); + String description = getCellValue(row, MaterialHeader.DESCRIPTION.ordinal(), rowNumber); + String hsCode = getCellValue(row, MaterialHeader.HS_CODE.ordinal(), rowNumber); + String operation = getCellValue(row, MaterialHeader.OPERATION.ordinal(), rowNumber); + + // Validate lengths + validateLength(partNumber, 0, 12, "Part Number", rowNumber); + validateLength(hsCode, 0, 11, "HS Code", rowNumber); + validateLength(description, 1, 500, "Description", rowNumber); + + // Validate operation enum + BulkInstructionType instructionType; + try { + instructionType = BulkInstructionType.valueOf(operation); + } catch (IllegalArgumentException e) { + throw new ExcelValidationError( + String.format("Invalid operation '%s' at row %d. Must be one of: %s", + operation, rowNumber, Arrays.toString(BulkInstructionType.values())) + ); + } + + // Set entity properties + entity.setPartNumber(partNumber); + entity.setName(description); + entity.setHsCode(hsCode); + entity.setNormalizedPartNumber(normalizePartNumber(partNumber)); + entity.setDeprecated(false); + + // Validate HS Code + if (!validateHsCode(entity.getHsCode())) { + throw new ExcelValidationError( + String.format("Invalid HS Code '%s' at row %d", hsCode, rowNumber) + ); + } + + return new BulkInstruction<>(entity, instructionType); + + } catch (ExcelValidationError e) { + throw e; + } catch (Exception e) { + throw new ExcelValidationError( + String.format("Error processing row %d: %s", rowNumber, e.getMessage()) + ); + } + } + + /** + * Gets a cell value as string with proper error handling + */ + private String getCellValue(Row row, int columnIndex, int rowNumber) { + return row.getCellAsString(columnIndex) + .orElseThrow(() -> new ExcelValidationError( + String.format("Missing value at row %d, column %d", rowNumber, columnIndex) + )); + } + + /** + * Validates the length of a field + */ + private void validateLength(String value, int minLength, int maxLength, String fieldName, int rowNumber) { + if (value.length() < minLength || value.length() > maxLength) { + throw new ExcelValidationError( + String.format("%s at row %d must be between %d and %d characters (found %d)", + fieldName, rowNumber, minLength, maxLength, value.length()) + ); + } + } + + /** + * Normalizes part number by padding with zeros + */ + private String normalizePartNumber(String partNumber) { + if (partNumber.length() > 12) { + throw new IllegalArgumentException("Part number must be less than 12 characters"); + } + return "000000000000".concat(partNumber).substring(partNumber.length()); + } + + /** + * Validates HS Code (placeholder for API validation) + */ + private boolean validateHsCode(String hsCode) { + //TODO check via api?! + return true; + } +} \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java index b2ac5c7..cdb748f 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java @@ -2,7 +2,6 @@ package de.avatic.lcc.service.transformer.nodes; import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO; import de.avatic.lcc.model.db.nodes.Node; -import org.apache.commons.lang3.NotImplementedException; import org.springframework.stereotype.Service; @Service @@ -12,7 +11,7 @@ public class NodeUpdateDTOTransformer { public Node fromNodeUpdateDTO(NodeUpdateDTO dto) { - throw new NotImplementedException("Not yet implemented fromNodeUpdateDTO"); + throw new IllegalStateException("Not yet implemented fromNodeUpdateDTO"); }