diff --git a/backend/pom.xml b/backend/pom.xml
index 1ec581bd9..b1d216b32 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -458,5 +458,10 @@
osx-aarch_64
test
+
+ org.apache.poi
+ poi-ooxml
+ 5.4.1
+
diff --git a/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceImpl.java b/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceImpl.java
index 0372057ca..78b7883b7 100644
--- a/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceImpl.java
+++ b/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceImpl.java
@@ -18,6 +18,8 @@ public class AttachmentConversionServiceImpl implements AttachmentConversionServ
private static final String CONTENT_TYPE_TIKA_OOXML = "application/x-tika-ooxml";
private static final String CONTENT_TYPE_DOC = "application/msword";
private static final String CONTENT_TYPE_TIKA_MSOFFICE = "application/x-tika-msoffice";
+ private static final String CONTENT_TYPE_TIKA_XLSX =
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private final AttachmentService attachmentService;
@@ -117,6 +119,7 @@ private FileTypeConverter getConverter(String contentType) {
}
case CONTENT_TYPE_DOCX, CONTENT_TYPE_TIKA_OOXML -> new DocxConverter();
case CONTENT_TYPE_DOC, CONTENT_TYPE_TIKA_MSOFFICE -> new DocConverter();
+ case CONTENT_TYPE_TIKA_XLSX -> new XlsxConverter();
default -> {
throw new IllegalArgumentException("Unsupported content type");
}
diff --git a/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/XlsxConverter.java b/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/XlsxConverter.java
new file mode 100644
index 000000000..79bd684eb
--- /dev/null
+++ b/backend/src/main/java/eu/bbmri_eric/negotiator/attachment/XlsxConverter.java
@@ -0,0 +1,171 @@
+package eu.bbmri_eric.negotiator.attachment;
+
+import eu.bbmri_eric.negotiator.common.exceptions.PdfGenerationException;
+import java.awt.Color;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+/** Converter for XLSX (Excel) files to PDF format. */
+@CommonsLog
+class XlsxConverter implements FileTypeConverter {
+ /** Page margin in points. */
+ private static final float MARGIN = 50;
+
+ /** Font size for cell text. */
+ private static final float FONT_SIZE = 10;
+
+ /** Line leading (spacing between rows). */
+ private static final float LEADING = 14;
+
+ /** Padding inside cells. */
+ private static final float CELL_PADDING = 5;
+
+ /** Minimum width for columns. */
+ private static final float MIN_COLUMN_WIDTH = 60;
+
+ @Override
+ public byte[] convertToPdf(final byte[] xlsxBytes) throws IOException, PdfGenerationException {
+ if (xlsxBytes == null || xlsxBytes.length == 0) {
+ throw new IllegalArgumentException("Input XLSX bytes are null or empty");
+ }
+
+ log.debug("Converting XLSX to PDF, input size: " + xlsxBytes.length);
+
+ try (ByteArrayInputStream xlsxInputStream = new ByteArrayInputStream(xlsxBytes);
+ Workbook workbook = new XSSFWorkbook(xlsxInputStream);
+ PDDocument document = new PDDocument();
+ ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+
+ for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
+ Sheet sheet = workbook.getSheetAt(sheetIndex);
+ if (sheet.getPhysicalNumberOfRows() > 0) {
+ convertSheetToPdf(document, sheet);
+ }
+ }
+
+ if (document.getNumberOfPages() == 0) {
+ document.addPage(new PDPage(PDRectangle.A4));
+ }
+
+ document.save(pdfOutputStream);
+ byte[] result = pdfOutputStream.toByteArray();
+ log.debug("Successfully converted XLSX to PDF, output size: " + result.length);
+ return result;
+ } catch (Exception e) {
+ log.error("Error converting XLSX to PDF: " + e.getMessage(), e);
+ throw new PdfGenerationException();
+ }
+ }
+
+ private void convertSheetToPdf(final PDDocument document, final Sheet sheet) throws IOException {
+ PDPage page = new PDPage(PDRectangle.A4);
+ document.addPage(page);
+
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true)) {
+
+ float pageWidth = page.getMediaBox().getWidth() - (2 * MARGIN);
+ float yPosition =
+ drawSheetHeader(contentStream, sheet.getSheetName(), page.getMediaBox().getHeight());
+
+ int maxCols = calculateMaxColumns(sheet);
+
+ float columnWidth = maxCols > 0 ? pageWidth / maxCols : MIN_COLUMN_WIDTH;
+ columnWidth = Math.max(columnWidth, MIN_COLUMN_WIDTH);
+
+ for (Row row : sheet) {
+ if (yPosition < MARGIN + LEADING) {
+ break;
+ }
+ drawRow(contentStream, row, yPosition, columnWidth, maxCols);
+ yPosition -= LEADING;
+ }
+ }
+ }
+
+ private float drawSheetHeader(
+ PDPageContentStream contentStream, String sheetName, float pageHeight) throws IOException {
+ float yPosition = pageHeight - MARGIN;
+ contentStream.beginText();
+ contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), FONT_SIZE + 2);
+ contentStream.newLineAtOffset(MARGIN, yPosition);
+ contentStream.showText("Sheet: " + sheetName);
+ contentStream.endText();
+ return yPosition - (LEADING * 2);
+ }
+
+ private int calculateMaxColumns(Sheet sheet) {
+ int maxCols = 0;
+ for (Row row : sheet) {
+ if (row.getLastCellNum() > maxCols) {
+ maxCols = row.getLastCellNum();
+ }
+ }
+ return maxCols;
+ }
+
+ private void drawRow(
+ PDPageContentStream contentStream, Row row, float yPosition, float columnWidth, int maxCols)
+ throws IOException {
+ float xPosition = MARGIN;
+ for (int cellIndex = 0; cellIndex < maxCols; cellIndex++) {
+ Cell cell = row.getCell(cellIndex);
+ drawCell(contentStream, cell, xPosition, yPosition, columnWidth);
+ xPosition += columnWidth;
+ }
+ }
+
+ private void drawCell(PDPageContentStream contentStream, Cell cell, float x, float y, float width)
+ throws IOException {
+ String cellValue = getCellValueAsString(cell);
+
+ contentStream.setStrokingColor(Color.LIGHT_GRAY);
+ contentStream.addRect(x, y - LEADING, width, LEADING);
+ contentStream.stroke();
+
+ if (cellValue != null && !cellValue.isEmpty()) {
+ contentStream.beginText();
+ contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
+ contentStream.newLineAtOffset(x + CELL_PADDING, y - FONT_SIZE);
+ String displayText = truncateText(cellValue, width - (2 * CELL_PADDING));
+ contentStream.showText(displayText);
+ contentStream.endText();
+ }
+ }
+
+ private String getCellValueAsString(final Cell cell) {
+ if (cell == null) {
+ return "";
+ }
+
+ return switch (cell.getCellType()) {
+ case STRING -> cell.getStringCellValue();
+ case NUMERIC -> String.valueOf(cell.getNumericCellValue());
+ case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
+ case FORMULA -> cell.getCellFormula();
+ case BLANK -> "";
+ default -> "";
+ };
+ }
+
+ private String truncateText(final String text, final float maxWidth) {
+ int maxChars = (int) (maxWidth / (FONT_SIZE * 0.6));
+ if (text.length() > maxChars) {
+ return text.substring(0, Math.max(0, maxChars - 3)) + "...";
+ }
+ return text;
+ }
+}
diff --git a/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceTest.java b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceTest.java
index 694c88e00..9d1d91732 100644
--- a/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceTest.java
+++ b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentConversionServiceTest.java
@@ -7,7 +7,6 @@
import eu.bbmri_eric.negotiator.attachment.dto.AttachmentDTO;
import java.io.IOException;
-import java.io.InputStream;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,7 +49,7 @@ void testconvertAttachmentsToPDF_WithPdfAttachment_ReturnsOriginalBytes() {
@Test
void testconvertAttachmentsToPdf_WithDocxAttachment_ConvertsSuccessfully() throws IOException {
String attachmentId = "docx-attachment-1";
- byte[] docxBytes = loadTestDocxFile();
+ byte[] docxBytes = AttachmentTestHelper.loadTestDocxFile();
AttachmentDTO docxAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -71,7 +70,7 @@ void testconvertAttachmentsToPdf_WithDocxAttachment_ConvertsSuccessfully() throw
@Test
void testconvertAttachmentsToPdf_WithDocAttachment() throws IOException {
String attachmentId = "doc-attachment-1";
- byte[] docBytes = loadTestDocFile();
+ byte[] docBytes = AttachmentTestHelper.loadTestDocFile();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -91,7 +90,7 @@ void testconvertAttachmentsToPdf_WithDocAttachment() throws IOException {
@Test
void testconvertAttachmentsToPdf_WithDocAttachment_SkipsInvalidDoc() throws IOException {
String attachmentId = "doc-attachment-1";
- byte[] docBytes = loadTestDocFile();
+ byte[] docBytes = AttachmentTestHelper.loadTestDocFile();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -111,7 +110,7 @@ void testconvertAttachmentsToPdf_WithDocAttachment_SkipsInvalidDoc() throws IOEx
@Test
void testconvertAttachmentsToPdf_WithTikaDocxType_ConvertsSuccessfully() throws IOException {
String attachmentId = "tika-docx-attachment-1";
- byte[] docxBytes = loadTestDocxFile();
+ byte[] docxBytes = AttachmentTestHelper.loadTestDocxFile();
AttachmentDTO docxAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -132,7 +131,7 @@ void testconvertAttachmentsToPdf_WithTikaDocxType_ConvertsSuccessfully() throws
@Test
void testconvertAttachmentsToPdf_WithTikaDocType_SkipsInvalidDoc() throws IOException {
String attachmentId = "tika-doc-attachment-1";
- byte[] docBytes = loadTestDocFile();
+ byte[] docBytes = AttachmentTestHelper.loadTestDocFile();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -152,7 +151,7 @@ void testconvertAttachmentsToPdf_WithTikaDocType_SkipsInvalidDoc() throws IOExce
@Test
void testconvertAttachmentsToPdf_WithTikaDocType() throws IOException {
String attachmentId = "tika-doc-attachment-1";
- byte[] docBytes = loadTestDocFileValid();
+ byte[] docBytes = AttachmentTestHelper.loadTestDocFileValid();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -175,7 +174,7 @@ void testconvertAttachmentsToPDF_WithMultipleAttachments_ProcessesAll() throws I
String docxId = "docx-1";
byte[] pdfBytes = "PDF content".getBytes();
- byte[] docxBytes = loadTestDocxFile();
+ byte[] docxBytes = AttachmentTestHelper.loadTestDocxFile();
AttachmentDTO pdfAttachment =
AttachmentDTO.builder()
@@ -519,7 +518,7 @@ void testconvertAttachmentsToPDF_WithMixedValidAndInvalidAttachments_ProcessesVa
String validDocxId = "valid-docx";
byte[] pdfBytes = "PDF content".getBytes();
- byte[] docxBytes = createMinimalDocxBytes();
+ byte[] docxBytes = AttachmentTestHelper.createMinimalDocxBytes();
AttachmentDTO validPdfAttachment =
AttachmentDTO.builder()
@@ -571,7 +570,7 @@ void testconvertAttachmentsToPDF_WithMixedValidAndInvalidAttachments_ProcessesVa
void testconvertAttachmentsToPdf_WithValidDocWithContent_ProcessesSuccessfully()
throws IOException {
String attachmentId = "valid-doc-with-content";
- byte[] docBytes = createValidDocBytes();
+ byte[] docBytes = AttachmentTestHelper.createValidDocBytes();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
.id(attachmentId)
@@ -731,7 +730,7 @@ void testConvertDocxToPdf_WithInvalidDocxPackage_ThrowsException() {
void testConvertDocToPdf_WithValidDocFile_ConvertsSuccessfully() {
// Test successful DOC to PDF conversion with a more realistic DOC structure
String attachmentId = "valid-doc-conversion";
- byte[] validDocBytes = createRealisticDocBytes();
+ byte[] validDocBytes = AttachmentTestHelper.createRealisticDocBytes();
AttachmentDTO docAttachment =
AttachmentDTO.builder()
@@ -761,234 +760,4 @@ void testConvertDocToPdf_WithValidDocFile_ConvertsSuccessfully() {
"Result should be a valid PDF or substantial content");
}
}
-
- private byte[] loadTestDocxFile() throws IOException {
- try (InputStream inputStream = getClass().getResourceAsStream("/test-documents/test.docx")) {
- if (inputStream == null) {
- return createMinimalDocxBytes();
- }
- return inputStream.readAllBytes();
- }
- }
-
- private byte[] loadTestDocFileValid() throws IOException {
- try (InputStream inputStream =
- getClass().getResourceAsStream("/test-documents/test-valid.doc")) {
- if (inputStream == null) {
- return createMinimalDocBytes();
- }
- return inputStream.readAllBytes();
- }
- }
-
- private byte[] loadTestDocFile() throws IOException {
- try (InputStream inputStream = getClass().getResourceAsStream("/test-documents/test.doc")) {
- if (inputStream == null) {
- return createMinimalDocBytes();
- }
- return inputStream.readAllBytes();
- }
- }
-
- private byte[] createMinimalDocxBytes() {
- String minimalDocx =
- "PK\u0003\u0004\u0014\u0000\u0000\u0000\u0008\u0000\u0000\u0000\u0000\u0000"
- + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
- + "\u0019\u0000\u0000\u0000[Content_Types].xmlPK\u0003\u0004\u0014\u0000"
- + "\u0000\u0000\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
- + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u000b\u0000\u0000\u0000"
- + "_rels/.relsPK\u0005\u0006\u0000\u0000\u0000\u0000\u0002\u0000\u0002\u0000"
- + "^\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000";
- return minimalDocx.getBytes();
- }
-
- private byte[] createMinimalDocBytes() {
- byte[] docHeader = {
- (byte) 0xD0,
- (byte) 0xCF,
- (byte) 0x11,
- (byte) 0xE0,
- (byte) 0xA1,
- (byte) 0xB1,
- (byte) 0x1A,
- (byte) 0xE1
- };
- byte[] docContent = new byte[512];
- System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
- return docContent;
- }
-
- private byte[] createValidDocBytes() {
- // Create a more complete DOC structure that might be parseable
- byte[] docHeader = {
- (byte) 0xD0,
- (byte) 0xCF,
- (byte) 0x11,
- (byte) 0xE0,
- (byte) 0xA1,
- (byte) 0xB1,
- (byte) 0x1A,
- (byte) 0xE1,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x3E,
- 0x00,
- 0x03,
- 0x00,
- (byte) 0xFE,
- (byte) 0xFF,
- 0x09,
- 0x00,
- 0x06,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x10,
- 0x00,
- 0x00
- };
- byte[] docContent = new byte[2048];
- System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
- return docContent;
- }
-
- private byte[] createValidEmptyDocBytes() {
- // Create a DOC structure that will parse but have no paragraphs
- byte[] docHeader = {
- (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0,
- (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1
- };
- byte[] docContent = new byte[1024];
- System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
- return docContent;
- }
-
- private byte[] createRealisticDocBytes() {
- // Create a more realistic DOC file structure that has a better chance of being parsed
- // This creates a minimal but more complete OLE2 compound document structure
- byte[] docContent = new byte[4096]; // Larger size for more realistic structure
-
- // OLE2 header (first 512 bytes are the header sector)
- byte[] oleHeader = {
- // OLE signature
- (byte) 0xD0,
- (byte) 0xCF,
- (byte) 0x11,
- (byte) 0xE0,
- (byte) 0xA1,
- (byte) 0xB1,
- (byte) 0x1A,
- (byte) 0xE1,
- // Minor version
- 0x00,
- 0x00,
- // Major version
- 0x3E,
- 0x00,
- // Byte order
- (byte) 0xFE,
- (byte) 0xFF,
- // Sector size (512 bytes = 2^9)
- 0x09,
- 0x00,
- // Mini sector size (64 bytes = 2^6)
- 0x06,
- 0x00,
- // Reserved fields
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- // Number of directory sectors
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- // Number of FAT sectors
- 0x01,
- 0x00,
- 0x00,
- 0x00,
- // Directory first sector
- 0x01,
- 0x00,
- 0x00,
- 0x00,
- // Transaction signature
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- // Mini stream cutoff (4096 bytes)
- 0x00,
- 0x10,
- 0x00,
- 0x00,
- // First mini FAT sector
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- // Number of mini FAT sectors
- 0x00,
- 0x00,
- 0x00,
- 0x00,
- // First difat sector
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF
- };
-
- System.arraycopy(oleHeader, 0, docContent, 0, oleHeader.length);
-
- // Fill remaining header with appropriate values
- // FAT array (starting at offset 76)
- int fatOffset = 76;
- // Sector 0 points to sector 1 (continuation)
- docContent[fatOffset] = (byte) 0xFF;
- docContent[fatOffset + 1] = (byte) 0xFF;
- docContent[fatOffset + 2] = (byte) 0xFF;
- docContent[fatOffset + 3] = (byte) 0xFE; // End of chain
-
- // Add some realistic Word document content in subsequent sectors
- // This creates a minimal Word document structure
- int contentOffset = 512; // Start of sector 1
- String wordContent = "Microsoft Word Document Content";
- byte[] contentBytes = wordContent.getBytes();
- System.arraycopy(
- contentBytes,
- 0,
- docContent,
- contentOffset,
- Math.min(contentBytes.length, docContent.length - contentOffset));
-
- return docContent;
- }
}
diff --git a/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentTestHelper.java b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentTestHelper.java
new file mode 100644
index 000000000..676a3e368
--- /dev/null
+++ b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/AttachmentTestHelper.java
@@ -0,0 +1,432 @@
+package eu.bbmri_eric.negotiator.attachment;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Supplier;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public final class AttachmentTestHelper {
+
+ private byte[] loadResourceOrDefault(String path, Supplier defaultSupplier)
+ throws IOException {
+ try (InputStream inputStream = AttachmentTestHelper.class.getResourceAsStream(path)) {
+ if (inputStream == null) {
+ return defaultSupplier.get();
+ }
+ return inputStream.readAllBytes();
+ }
+ }
+
+ public byte[] loadTestDocxFile() throws IOException {
+ return loadResourceOrDefault(
+ "/test-documents/test.docx", AttachmentTestHelper::createMinimalDocxBytes);
+ }
+
+ public byte[] loadTestDocFileValid() throws IOException {
+ return loadResourceOrDefault(
+ "/test-documents/test-valid.doc", AttachmentTestHelper::createMinimalDocBytes);
+ }
+
+ public byte[] loadTestDocFile() throws IOException {
+ return loadResourceOrDefault(
+ "/test-documents/test.doc", AttachmentTestHelper::createMinimalDocBytes);
+ }
+
+ public byte[] createMinimalDocxBytes() {
+ String minimalDocx =
+ "PK\u0003\u0004\u0014\u0000\u0000\u0000\u0008\u0000\u0000\u0000\u0000\u0000"
+ + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
+ + "\u0019\u0000\u0000\u0000[Content_Types].xmlPK\u0003\u0004\u0014\u0000"
+ + "\u0000\u0000\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"
+ + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u000b\u0000\u0000\u0000"
+ + "_rels/.relsPK\u0005\u0006\u0000\u0000\u0000\u0000\u0002\u0000\u0002\u0000"
+ + "^\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000";
+ return minimalDocx.getBytes();
+ }
+
+ public byte[] createMinimalDocBytes() {
+ byte[] docHeader = {
+ (byte) 0xD0,
+ (byte) 0xCF,
+ (byte) 0x11,
+ (byte) 0xE0,
+ (byte) 0xA1,
+ (byte) 0xB1,
+ (byte) 0x1A,
+ (byte) 0xE1
+ };
+ byte[] docContent = new byte[512];
+ System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
+ return docContent;
+ }
+
+ public byte[] createValidDocBytes() {
+ // Create a more complete DOC structure that might be parseable
+ byte[] docHeader = {
+ (byte) 0xD0,
+ (byte) 0xCF,
+ (byte) 0x11,
+ (byte) 0xE0,
+ (byte) 0xA1,
+ (byte) 0xB1,
+ (byte) 0x1A,
+ (byte) 0xE1,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x3E,
+ 0x00,
+ 0x03,
+ 0x00,
+ (byte) 0xFE,
+ (byte) 0xFF,
+ 0x09,
+ 0x00,
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x10,
+ 0x00,
+ 0x00
+ };
+ byte[] docContent = new byte[2048];
+ System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
+ return docContent;
+ }
+
+ public byte[] createValidEmptyDocBytes() {
+ // Create a DOC structure that will parse but have no paragraphs
+ byte[] docHeader = {
+ (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0,
+ (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1
+ };
+ byte[] docContent = new byte[1024];
+ System.arraycopy(docHeader, 0, docContent, 0, docHeader.length);
+ return docContent;
+ }
+
+ public byte[] createRealisticDocBytes() {
+ // Create a more realistic DOC file structure that has a better chance of being parsed
+ // This creates a minimal but more complete OLE2 compound document structure
+ byte[] docContent = new byte[4096]; // Larger size for more realistic structure
+
+ // OLE2 header (first 512 bytes are the header sector)
+ byte[] oleHeader = {
+ // OLE signature
+ (byte) 0xD0,
+ (byte) 0xCF,
+ (byte) 0x11,
+ (byte) 0xE0,
+ (byte) 0xA1,
+ (byte) 0xB1,
+ (byte) 0x1A,
+ (byte) 0xE1,
+ // Minor version
+ 0x00,
+ 0x00,
+ // Major version
+ 0x3E,
+ 0x00,
+ // Byte order
+ (byte) 0xFE,
+ (byte) 0xFF,
+ // Sector size (512 bytes = 2^9)
+ 0x09,
+ 0x00,
+ // Mini sector size (64 bytes = 2^6)
+ 0x06,
+ 0x00,
+ // Reserved fields
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Number of directory sectors
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Number of FAT sectors
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Directory first sector
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Transaction signature
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // Mini stream cutoff (4096 bytes)
+ 0x00,
+ 0x10,
+ 0x00,
+ 0x00,
+ // First mini FAT sector
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ // Number of mini FAT sectors
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ // First difat sector
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF
+ };
+
+ System.arraycopy(oleHeader, 0, docContent, 0, oleHeader.length);
+
+ // Fill remaining header with appropriate values
+ // FAT array (starting at offset 76)
+ int fatOffset = 76;
+ // Sector 0 points to sector 1 (continuation)
+ docContent[fatOffset] = (byte) 0xFF;
+ docContent[fatOffset + 1] = (byte) 0xFF;
+ docContent[fatOffset + 2] = (byte) 0xFF;
+ docContent[fatOffset + 3] = (byte) 0xFE; // End of chain
+
+ // Add some realistic Word document content in subsequent sectors
+ // This creates a minimal Word document structure
+ int contentOffset = 512; // Start of sector 1
+ String wordContent = "Microsoft Word Document Content";
+ byte[] contentBytes = wordContent.getBytes();
+ System.arraycopy(
+ contentBytes,
+ 0,
+ docContent,
+ contentOffset,
+ Math.min(contentBytes.length, docContent.length - contentOffset));
+
+ return docContent;
+ }
+
+ public byte[] createValidXlsxBytes() throws IOException {
+ return loadResourceOrDefault(
+ "/test-documents/test.xlsx",
+ () -> {
+ try {
+ return createMinimalValidXlsxBytes();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to generate fallback XLSX", e);
+ }
+ });
+ }
+
+ public byte[] createMinimalValidXlsxBytes() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("Sheet1");
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(0);
+ org.apache.poi.ss.usermodel.Cell cell = row.createCell(0);
+ cell.setCellValue("Test");
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createEmptyXlsxBytes() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ workbook.createSheet("EmptySheet");
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithMultipleSheets() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet1 = workbook.createSheet("Sheet1");
+ org.apache.poi.ss.usermodel.Row row1 = sheet1.createRow(0);
+ row1.createCell(0).setCellValue("Sheet 1 Data");
+ row1.createCell(1).setCellValue("Column 2");
+
+ org.apache.poi.ss.usermodel.Sheet sheet2 = workbook.createSheet("Sheet2");
+ org.apache.poi.ss.usermodel.Row row2 = sheet2.createRow(0);
+ row2.createCell(0).setCellValue("Sheet 2 Data");
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithFormulas() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("Formulas");
+ org.apache.poi.ss.usermodel.Row row1 = sheet.createRow(0);
+ row1.createCell(0).setCellValue(10);
+ row1.createCell(1).setCellValue(20);
+
+ org.apache.poi.ss.usermodel.Row row2 = sheet.createRow(1);
+
+ org.apache.poi.ss.usermodel.Cell formulaCell = row2.createCell(0);
+ formulaCell.setCellFormula("SUM(A1:B1)");
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithVariousCellTypes() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("Types");
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(0);
+
+ row.createCell(0).setCellValue("Text");
+ row.createCell(1).setCellValue(123.45);
+ row.createCell(2).setCellValue(true);
+ org.apache.poi.ss.usermodel.Cell formulaCell = row.createCell(3);
+ formulaCell.setCellFormula("B1*2");
+ row.createCell(4).setBlank();
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithLongText() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("LongText");
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(0);
+
+ String longText =
+ "This is a very long text that should be truncated when rendering to PDF because it"
+ + " exceeds the maximum width allowed for a cell in the PDF output format and we"
+ + " need to test the truncation logic";
+ row.createCell(0).setCellValue(longText);
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithShortText() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("ShortText");
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(0);
+ row.createCell(0).setCellValue("Short");
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithManyRows() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("ManyRows");
+
+ for (int i = 0; i < 60; i++) {
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(i);
+ row.createCell(0).setCellValue("Row " + i);
+ row.createCell(1).setCellValue(i * 10);
+ }
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithEmptyCells() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("EmptyCells");
+ org.apache.poi.ss.usermodel.Row row1 = sheet.createRow(0);
+
+ row1.createCell(0).setCellValue("A1");
+ row1.createCell(2).setCellValue("C1");
+
+ org.apache.poi.ss.usermodel.Row row2 = sheet.createRow(1);
+
+ row2.createCell(1).setCellValue("B2");
+ row2.createCell(3).setCellValue("");
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithSingleColumn() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("SingleColumn");
+
+ for (int i = 0; i < 5; i++) {
+ org.apache.poi.ss.usermodel.Row row = sheet.createRow(i);
+ row.createCell(0).setCellValue("Value " + i);
+ }
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+
+ public byte[] createXlsxWithBooleanAndBlankCells() throws IOException {
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+ org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("BooleanBlank");
+ org.apache.poi.ss.usermodel.Row row1 = sheet.createRow(0);
+
+ row1.createCell(0).setCellValue(true);
+ row1.createCell(1).setCellValue(false);
+ row1.createCell(2).setBlank();
+
+ org.apache.poi.ss.usermodel.Row row2 = sheet.createRow(1);
+ row2.createCell(0).setCellValue("After Boolean");
+ row2.createCell(1).setBlank();
+ row2.createCell(2).setCellValue(true);
+
+ workbook.write(baos);
+ }
+ return baos.toByteArray();
+ }
+}
diff --git a/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/XlsxConverterTest.java b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/XlsxConverterTest.java
new file mode 100644
index 000000000..2d3dc6a31
--- /dev/null
+++ b/backend/src/test/java/eu/bbmri_eric/negotiator/attachment/XlsxConverterTest.java
@@ -0,0 +1,403 @@
+package eu.bbmri_eric.negotiator.attachment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+import eu.bbmri_eric.negotiator.attachment.dto.AttachmentDTO;
+import java.io.IOException;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class XlsxConverterTest {
+
+ @Mock private AttachmentService attachmentService;
+
+ private AttachmentConversionServiceImpl conversionService;
+
+ @BeforeEach
+ void setUp() {
+ conversionService = new AttachmentConversionServiceImpl(attachmentService);
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithXlsxAttachment_ConvertsSuccessfully() throws IOException {
+ String attachmentId = "xlsx-attachment-1";
+ byte[] xlsxBytes = AttachmentTestHelper.createValidXlsxBytes();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+
+ byte[] pdfBytes = result.get(0);
+ String pdfHeader = new String(pdfBytes, 0, Math.min(4, pdfBytes.length));
+ assertTrue(pdfHeader.startsWith("%PDF"));
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithXlsxNullPayload_SkipsAttachment() {
+ String attachmentId = "xlsx-null-payload";
+ AttachmentDTO attachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(null)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(attachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithXlsxEmptyPayload_SkipsAttachment() {
+ String attachmentId = "xlsx-empty-payload";
+ AttachmentDTO attachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(new byte[0])
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(attachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithInvalidXlsxBytes_SkipsAttachment() {
+ String attachmentId = "invalid-xlsx-1";
+ AttachmentDTO invalidXlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("invalid.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload("Invalid XLSX content".getBytes())
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(invalidXlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithCorruptedXlsxFile_SkipsAttachment() {
+ String attachmentId = "corrupted-xlsx-1";
+ byte[] corruptedXlsxBytes = "This is not a valid XLSX file".getBytes();
+ AttachmentDTO corruptedXlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("corrupted.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(corruptedXlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(corruptedXlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithMultipleXlsxSheets_ConvertsSuccessfully()
+ throws IOException {
+ String attachmentId = "multi-sheet-xlsx";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithMultipleSheets();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("multi-sheet.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+
+ byte[] pdfBytes = result.get(0);
+ String pdfHeader = new String(pdfBytes, 0, Math.min(4, pdfBytes.length));
+ assertTrue(pdfHeader.startsWith("%PDF"));
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithEmptyXlsxWorkbook_ConvertsSuccessfully() throws IOException {
+ String attachmentId = "empty-xlsx";
+ byte[] xlsxBytes = AttachmentTestHelper.createEmptyXlsxBytes();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("empty.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithMixedDocxAndXlsx_ProcessesBoth() throws IOException {
+ String docxId = "docx-1";
+ String xlsxId = "xlsx-1";
+
+ byte[] docxBytes = AttachmentTestHelper.loadTestDocxFile();
+ byte[] xlsxBytes = AttachmentTestHelper.createValidXlsxBytes();
+
+ AttachmentDTO docxAttachment =
+ AttachmentDTO.builder()
+ .id(docxId)
+ .name("test.docx")
+ .contentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
+ .payload(docxBytes)
+ .build();
+
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(xlsxId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(docxId)).thenReturn(docxAttachment);
+ when(attachmentService.findById(xlsxId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(docxId, xlsxId));
+
+ assertEquals(2, result.size());
+ assertTrue(result.get(0).length > 0);
+ assertTrue(result.get(1).length > 0);
+ }
+
+ @Test
+ void testConvertAttachmentsToPdf_WithXlsxWithFormulas_ConvertsSuccessfully() throws IOException {
+ String attachmentId = "xlsx-with-formulas";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithFormulas();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("formulas.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithNullBytes_ThrowsIllegalArgumentException() {
+ String attachmentId = "xlsx-null-bytes";
+ AttachmentDTO attachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(null)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(attachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testXlsxConverter_WithEmptyBytes_ThrowsIllegalArgumentException() {
+ String attachmentId = "xlsx-empty-bytes";
+ AttachmentDTO attachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("test.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(new byte[0])
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(attachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testXlsxConverter_WithVariousCellTypes_HandlesAllTypes() throws IOException {
+ String attachmentId = "xlsx-various-cell-types";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithVariousCellTypes();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("various-types.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+
+ byte[] pdfBytes = result.get(0);
+ String pdfHeader = new String(pdfBytes, 0, Math.min(4, pdfBytes.length));
+ assertTrue(pdfHeader.startsWith("%PDF"));
+ }
+
+ @Test
+ void testXlsxConverter_WithLongText_TruncatesCorrectly() throws IOException {
+ String attachmentId = "xlsx-long-text";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithLongText();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("long-text.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithShortText_DoesNotTruncate() throws IOException {
+ String attachmentId = "xlsx-short-text";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithShortText();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("short-text.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithManyRows_HandlesPageBreak() throws IOException {
+ String attachmentId = "xlsx-many-rows";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithManyRows();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("many-rows.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithEmptyCells_HandlesCorrectly() throws IOException {
+ String attachmentId = "xlsx-empty-cells";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithEmptyCells();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("empty-cells.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithSingleColumn_UsesMinWidth() throws IOException {
+ String attachmentId = "xlsx-single-column";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithSingleColumn();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("single-column.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+
+ @Test
+ void testXlsxConverter_WithBooleanAndBlankCells_HandlesCorrectly() throws IOException {
+ String attachmentId = "xlsx-boolean-blank";
+ byte[] xlsxBytes = AttachmentTestHelper.createXlsxWithBooleanAndBlankCells();
+ AttachmentDTO xlsxAttachment =
+ AttachmentDTO.builder()
+ .id(attachmentId)
+ .name("boolean-blank.xlsx")
+ .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .payload(xlsxBytes)
+ .build();
+
+ when(attachmentService.findById(attachmentId)).thenReturn(xlsxAttachment);
+
+ List result = conversionService.listToPdf(List.of(attachmentId));
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).length > 0);
+ }
+}