package net.gepafin.tendermanagement.util; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3URI; import com.amazonaws.services.s3.model.S3Object; import net.gepafin.tendermanagement.model.request.AttachmentRequest; import org.apache.poi.xwpf.usermodel.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.Base64; public class S3DocxProcessor { private final AmazonS3 s3Client; public static final Logger log = LoggerFactory.getLogger(S3DocxProcessor.class); public S3DocxProcessor(AmazonS3 s3Client) { this.s3Client = s3Client; } /** * Fetches DOCX files from S3, replaces placeholders, and returns updated files * as AttachmentRequest objects (ready for PEC API). * * @param s3Urls List of S3 file URLs * @param replacements Map of placeholder -> value * @return Map of original file name -> AttachmentRequest * @throws IOException */ public Map processFiles(List s3Urls, Map replacements) throws IOException { Map processedFiles = new HashMap<>(); for (String s3Url : s3Urls) { // Extract bucket & key from URL AmazonS3URI s3Uri = new AmazonS3URI(s3Url); String bucket = s3Uri.getBucket(); String key = s3Uri.getKey(); try (S3Object s3Object = s3Client.getObject(bucket, key); InputStream originalStream = new BufferedInputStream(s3Object.getObjectContent())) { byte[] updatedBytes=null; if (isDocxFile(originalStream)) { log.warn("Skipping non-DOCX file from S3: bucket={}, key={}", bucket, key); updatedBytes = replacePlaceholders(originalStream, replacements); }else { // non-DOCX → just copy raw stream updatedBytes = originalStream.readAllBytes(); log.info("Skipped placeholder replacement, copied file as-is: {}", key); } // convert to base64 String base64 = Base64.getEncoder().encodeToString(updatedBytes); String fileString = "data:application/octet-stream;base64," + base64; // create attachment request AttachmentRequest attachment = new AttachmentRequest(); attachment.setName(extractFileName(key)); attachment.setFile(fileString); processedFiles.put(key, attachment); } } return processedFiles; } private byte[] replacePlaceholders(InputStream docInputStream, Map replacements) throws IOException { try (XWPFDocument document = new XWPFDocument(docInputStream); ByteArrayOutputStream out = new ByteArrayOutputStream()) { // replace in body paragraphs replaceInParagraphs(document.getParagraphs(), replacements); // replace inside tables for (XWPFTable table : document.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { replaceInParagraphs(cell.getParagraphs(), replacements); } } } document.write(out); return out.toByteArray(); } } private void replaceInParagraphs(List paragraphs, Map replacements) { for (XWPFParagraph paragraph : paragraphs) { String paragraphText = paragraph.getText(); if (paragraphText != null && !paragraphText.isEmpty()) { // Apply all replacements for (Map.Entry entry : replacements.entrySet()) { paragraphText = paragraphText.replace(entry.getKey(), entry.getValue()); } // Remove old runs int runCount = paragraph.getRuns().size(); for (int i = runCount - 1; i >= 0; i--) { paragraph.removeRun(i); } // Add one new run with replaced text XWPFRun newRun = paragraph.createRun(); newRun.setText(paragraphText); } } } private String extractFileName(String key) { int lastSlash = key.lastIndexOf('/'); if (lastSlash >= 0 && lastSlash < key.length() - 1) { return key.substring(lastSlash + 1); } return key; } private boolean isDocxFile(InputStream inputStream) throws IOException { inputStream.mark(4); byte[] header = new byte[2]; int read = inputStream.read(header, 0, 2); inputStream.reset(); // DOCX = ZIP = should start with PK (0x50 0x4B) return read == 2 && header[0] == 0x50 && header[1] == 0x4B; } }