diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPage.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPage.java new file mode 100644 index 0000000..5761615 --- /dev/null +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPage.java @@ -0,0 +1,88 @@ +package com.ffii.lioner.modules.lioner.pdf.entity; + +import java.time.LocalDate; + +import com.ffii.core.entity.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Entity +@Table(name = "form_sig_page") +public class FormSigPage extends BaseEntity { + + @NotBlank + @Column(name = "formCode") + private String formCode; + + @NotNull + @Column(name = "startDate") + private LocalDate startDate; + + @NotBlank + @Column(name = "sigType") + private String sigType; + + @NotNull + @Column(name = "pageFrom") + private Integer pageFrom; + + @NotNull + @Column(name = "pageTo") + private Integer pageTo; + + @NotBlank + @Column(name = "action") + private String action; + + public String getFormCode() { + return formCode; + } + + public void setFormCode(String formCode) { + this.formCode = formCode; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public String getSigType() { + return sigType; + } + + public void setSigType(String sigType) { + this.sigType = sigType; + } + + public Integer getPageFrom() { + return pageFrom; + } + + public void setPageFrom(Integer pageFrom) { + this.pageFrom = pageFrom; + } + + public Integer getPageTo() { + return pageTo; + } + + public void setPageTo(Integer pageTo) { + this.pageTo = pageTo; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPageRepository.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPageRepository.java new file mode 100644 index 0000000..8cda4a5 --- /dev/null +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPageRepository.java @@ -0,0 +1,14 @@ +package com.ffii.lioner.modules.lioner.pdf.entity; + +import java.time.LocalDate; +import java.util.List; + +import com.ffii.core.support.AbstractRepository; + +public interface FormSigPageRepository extends AbstractRepository { + + List findByFormCodeAndSigTypeAndStartDateLessThanEqualAndDeletedFalseOrderByStartDateDesc( + String formCode, String sigType, LocalDate asOfDate); + + List findByDeletedFalseOrderByFormCodeAscStartDateDesc(); +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/req/UpdateFormSigPageReq.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/req/UpdateFormSigPageReq.java new file mode 100644 index 0000000..e891d50 --- /dev/null +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/req/UpdateFormSigPageReq.java @@ -0,0 +1,89 @@ +package com.ffii.lioner.modules.lioner.pdf.req; + +import java.time.LocalDate; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class UpdateFormSigPageReq { + + private Long id; + + @NotBlank + @Size(max = 50) + private String formCode; + + @NotNull + private LocalDate startDate; + + @NotBlank + @Size(max = 20) + private String sigType; + + @NotNull + private Integer pageFrom; + + @NotNull + private Integer pageTo; + + @NotBlank + @Size(max = 30) + private String action; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFormCode() { + return formCode; + } + + public void setFormCode(String formCode) { + this.formCode = formCode; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public String getSigType() { + return sigType; + } + + public void setSigType(String sigType) { + this.sigType = sigType; + } + + public Integer getPageFrom() { + return pageFrom; + } + + public void setPageFrom(Integer pageFrom) { + this.pageFrom = pageFrom; + } + + public Integer getPageTo() { + return pageTo; + } + + public void setPageTo(Integer pageTo) { + this.pageTo = pageTo; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/FormSigPageService.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/FormSigPageService.java new file mode 100644 index 0000000..e57f0af --- /dev/null +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/FormSigPageService.java @@ -0,0 +1,149 @@ +package com.ffii.lioner.modules.lioner.pdf.service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ffii.lioner.modules.lioner.pdf.entity.FormSigPage; +import com.ffii.lioner.modules.lioner.pdf.entity.FormSigPageRepository; +import com.ffii.lioner.modules.lioner.pdf.req.UpdateFormSigPageReq; + +@Service +public class FormSigPageService { + + private static final String UPLOAD1 = "upload1"; + private static final String UPLOAD2 = "upload2"; + + private final FormSigPageRepository formSigPageRepository; + + public FormSigPageService(FormSigPageRepository formSigPageRepository) { + this.formSigPageRepository = formSigPageRepository; + } + + /** List all non-deleted records, optionally filtered by formCode and sigType. */ + public List list(String formCode, String sigType) { + List all = formSigPageRepository.findByDeletedFalseOrderByFormCodeAscStartDateDesc(); + if (formCode != null && !formCode.isBlank()) { + all = all.stream().filter(f -> formCode.equals(f.getFormCode())).collect(Collectors.toList()); + } + if (sigType != null && !sigType.isBlank()) { + all = all.stream().filter(f -> sigType.equals(f.getSigType())).collect(Collectors.toList()); + } + return all; + } + + public Optional find(Long id) { + return formSigPageRepository.findById(id).filter(f -> !Boolean.TRUE.equals(f.getDeleted())); + } + + @Transactional(rollbackFor = Exception.class) + public FormSigPage save(UpdateFormSigPageReq req) { + FormSigPage entity; + // Only update when id is present and positive (existing record); otherwise create (id null or <= 0) + if (req.getId() != null && req.getId() > 0) { + entity = formSigPageRepository.findById(req.getId()).orElseThrow(); + entity.setFormCode(req.getFormCode()); + entity.setStartDate(req.getStartDate()); + entity.setSigType(req.getSigType()); + entity.setPageFrom(req.getPageFrom()); + entity.setPageTo(req.getPageTo()); + entity.setAction(req.getAction()); + } else { + entity = new FormSigPage(); + entity.setFormCode(req.getFormCode()); + entity.setStartDate(req.getStartDate()); + entity.setSigType(req.getSigType()); + entity.setPageFrom(req.getPageFrom()); + entity.setPageTo(req.getPageTo()); + entity.setAction(req.getAction()); + } + return formSigPageRepository.save(entity); + } + + @Transactional(rollbackFor = Exception.class) + public void markDelete(Long id) { + formSigPageRepository.findById(id).ifPresent(entity -> { + entity.setDeleted(Boolean.TRUE); + formSigPageRepository.save(entity); + }); + } + + /** + * Resolves the form_sig_page record valid for the given formCode and as-of date for a sig type. + * Uses the latest startDate <= asOfDate. + */ + public Optional getConfig(String formCode, LocalDate asOfDate, String sigType) { + if (formCode == null || asOfDate == null || sigType == null) { + return Optional.empty(); + } + List list = formSigPageRepository + .findByFormCodeAndSigTypeAndStartDateLessThanEqualAndDeletedFalseOrderByStartDateDesc( + formCode, sigType, asOfDate); + return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); + } + + public Optional getUpload1Config(String formCode, LocalDate asOfDate) { + return getConfig(formCode, asOfDate, UPLOAD1); + } + + public Optional getUpload2Config(String formCode, LocalDate asOfDate) { + return getConfig(formCode, asOfDate, UPLOAD2); + } + + /** + * Converts LocalDateTime (e.g. filled_form.created) to LocalDate for lookup. + */ + public Optional getUpload1Config(String formCode, LocalDateTime asOf) { + return asOf == null ? Optional.empty() : getUpload1Config(formCode, asOf.toLocalDate()); + } + + public Optional getUpload2Config(String formCode, LocalDateTime asOf) { + return asOf == null ? Optional.empty() : getUpload2Config(formCode, asOf.toLocalDate()); + } + + /** + * Returns config for both upload1 and upload2 for the given formCode and asOf date. + * Used by merge service and by the API for frontend labels. + */ + public Map getConfigForForm(String formCode, LocalDate asOfDate) { + Map map = new HashMap<>(); + getUpload1Config(formCode, asOfDate).ifPresent(c -> map.put(UPLOAD1, c)); + getUpload2Config(formCode, asOfDate).ifPresent(c -> map.put(UPLOAD2, c)); + return map; + } + + public Map getConfigForForm(String formCode, LocalDateTime asOf) { + return asOf == null ? Map.of() : getConfigForForm(formCode, asOf.toLocalDate()); + } + + /** + * Returns DTOs for API: list of { sigType, pageFrom, pageTo, label }. + */ + public List> getConfigDtosForForm(String formCode, LocalDate asOfDate) { + return getConfigForForm(formCode, asOfDate).values().stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + public List> getConfigDtosForForm(String formCode, LocalDateTime asOf) { + return asOf == null ? List.of() : getConfigDtosForForm(formCode, asOf.toLocalDate()); + } + + private Map toDto(FormSigPage c) { + Map m = new HashMap<>(); + m.put("sigType", c.getSigType()); + m.put("pageFrom", c.getPageFrom()); + m.put("pageTo", c.getPageTo()); + m.put("label", c.getPageFrom().equals(c.getPageTo()) + ? "Upload Page " + c.getPageFrom() + : "Upload Page " + c.getPageFrom() + "-" + c.getPageTo()); + return m; + } +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java index 22ca6f6..a2f8ccd 100644 --- a/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java @@ -4,12 +4,15 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.time.LocalDate; +import java.util.Optional; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.springframework.stereotype.Service; +import com.ffii.lioner.modules.lioner.pdf.entity.FormSigPage; import com.itextpdf.forms.PdfAcroForm; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; @@ -19,394 +22,250 @@ import com.itextpdf.kernel.utils.PdfMerger; @Service public class PdfMergeService { + private static final String SKIP_AND_APPEND = "SKIP_AND_APPEND"; + + private final FormSigPageService formSigPageService; + + public PdfMergeService(FormSigPageService formSigPageService) { + this.formSigPageService = formSigPageService; + } + // --- ACROFORM REMOVAL HELPER METHOD --- - + /** - * Flattens form fields, preserving their appearance (text/checkmarks) + * Flattens form fields, preserving their appearance (text/checkmarks) * but making the PDF plain and non-interactive. */ private void flattenPdf(PdfDocument doc) { - // Get the AcroForm instance for the document. 'true' creates it if none exists. PdfAcroForm acroForm = PdfAcroForm.getAcroForm(doc, true); - - // This method automatically converts all field values into static content - // and removes the form dictionary (making the PDF plain). - acroForm.flattenFields(); + acroForm.flattenFields(); } public void removePdfPassword() throws IOException { - // Load PDF A and PDF B - //String filePathA = "C:\\dev\\pdf\\pdfA.pdf"; - //File fileA = new File(filePathA); - String filePathB = "C:\\dev\\pdf\\pdfB.pdf"; File fileB = new File(filePathB); - String outputPath = "C:\\dev\\pdf\\pdfOut.pdf"; try ( - //PDDocument pdfA = Loader.loadPDF(fileA); PDDocument pdfB = Loader.loadPDF(fileB); - PDDocument mergedPdf = new PDDocument()) { for (int i = 0; i < pdfB.getNumberOfPages(); i++) { PDPage page = pdfB.getPage(i); mergedPdf.addPage(page); } - - //this is for remove the password and copy the whole pdf pdfB.setAllSecurityToBeRemoved(true); pdfB.save(new File(outputPath)); - - // Save the merged PDF mergedPdf.save(new File(outputPath)); mergedPdf.close(); } } - public void mergePdfs() throws IOException { - // This method uses PDFBox and is left unchanged as the focus was iText 7 String filePathA = "C:\\dev\\pdf\\pdfA.pdf"; File fileA = new File(filePathA); - String filePathB = "C:\\dev\\pdf\\pdfB.pdf"; File fileB = new File(filePathB); - String outputPath = "C:\\dev\\pdf\\pdfOut.pdf"; try ( PDDocument pdfA = Loader.loadPDF(fileA); PDDocument pdfB = Loader.loadPDF(fileB); - PDDocument mergedPdf = new PDDocument()) { for (int i = 0; i < pdfB.getNumberOfPages(); i++) { PDPage page = pdfB.getPage(i); mergedPdf.addPage(page); } - mergedPdf.save(new File(outputPath)); } } public byte[] flatPdfsItext7(byte[] pdfABytes) throws IOException { - - // --- STEP 1: Flatten PDF A and get the modified bytes --- byte[] pdfAFlattenedBytes; try (PdfReader readerA = new PdfReader(new ByteArrayInputStream(pdfABytes)); ByteArrayOutputStream tempBaosA = new ByteArrayOutputStream(); PdfWriter tempWriterA = new PdfWriter(tempBaosA); PdfDocument docA_mod = new PdfDocument(readerA, tempWriterA)) { - + flattenPdf(docA_mod); - docA_mod.close(); // IMPORTANT: Close to finalize writing to tempBaosA + docA_mod.close(); pdfAFlattenedBytes = tempBaosA.toByteArray(); return tempBaosA.toByteArray(); } - } - public byte[] mergePdfsItext7(String formCode, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + /** + * Merge upload1 (signature) PDF using config from form_sig_page. Uses asOfDate for versioning. + */ + public byte[] mergePdfsItext7(String formCode, LocalDate asOfDate, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + Optional configOpt = formSigPageService.getUpload1Config(formCode, asOfDate); - // Defined constants for clarity - final int IDA_SIG_PAGE = 15; // Page to skip for IDA - final int FNA_SIG_PAGE = 10; // Page to skip for FNA - final int HSBC_REP_PAGE = 11; // Page to replace for HSBCFIN - final int HSBCA31_REP_START = 28; // Start page to replace for HSBCA31 (Page 28) - final int HSBCA31_REP_END = 29; // End page to replace for HSBCA31 (Page 29) - final int HSBCA31_REP_COUNT = 2; // Number of pages from pdfB to insert - - final int MLB03S_REP_PAGE = 9; - final int MLFNA_REP_PAGE = 4; - - final int SLFNA_REP_PAGE = 5; - final int SLAPP_REP_PAGE = 17; - final int SLGII_REP_PAGE = 13; - - // --- STEP 1: Flatten PDF A and get the modified bytes --- byte[] pdfAFlattenedBytes; try (PdfReader readerA = new PdfReader(new ByteArrayInputStream(pdfABytes)); ByteArrayOutputStream tempBaosA = new ByteArrayOutputStream(); PdfWriter tempWriterA = new PdfWriter(tempBaosA); PdfDocument docA_mod = new PdfDocument(readerA, tempWriterA)) { - + flattenPdf(docA_mod); - docA_mod.close(); // IMPORTANT: Close to finalize writing to tempBaosA + docA_mod.close(); pdfAFlattenedBytes = tempBaosA.toByteArray(); } - // --- STEP 2: Flatten PDF B and get the modified bytes (if needed) --- byte[] pdfBFlattenedBytes = null; if (pdfBBytes != null && pdfBBytes.length > 0) { try (PdfReader readerB = new PdfReader(new ByteArrayInputStream(pdfBBytes)); ByteArrayOutputStream tempBaosB = new ByteArrayOutputStream(); PdfWriter tempWriterB = new PdfWriter(tempBaosB); PdfDocument docB_mod = new PdfDocument(readerB, tempWriterB)) { - + flattenPdf(docB_mod); - docB_mod.close(); // IMPORTANT: Close to finalize writing to tempBaosB + docB_mod.close(); pdfBFlattenedBytes = tempBaosB.toByteArray(); } } - // --- STEP 3: Perform the merge using the flattened bytes --- try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter writer = new PdfWriter(baos); PdfDocument mergedPdf = new PdfDocument(writer); PdfReader readerA_merge = new PdfReader(new ByteArrayInputStream(pdfAFlattenedBytes)); - PdfDocument docA = new PdfDocument(readerA_merge)) { + PdfDocument docA = new PdfDocument(readerA_merge)) { - // ⭐️ FIX: PdfMerger is NOT AutoCloseable, so we instantiate it here. PdfMerger merger = new PdfMerger(mergedPdf); - int totalPagesA = docA.getNumberOfPages(); - - // --- Multi/Single Page Replacement Logic --- - int repPage = -1; - int repStartA = -1; - int repEndA = -1; - int repCountB = -1; - boolean isMultiPageReplace = false; - - // Single Page Replacements - if ("MLB03S".equals(formCode)) { - repPage = MLB03S_REP_PAGE; - } else if ("MLFNA_EN".equals(formCode) || "MLFNA_CHI".equals(formCode)) { - repPage = MLFNA_REP_PAGE; - } else if ("SLFNA_EN".equals(formCode) || "SLFNA_CHI".equals(formCode)) { - repPage = SLFNA_REP_PAGE; - } else if ("SLAPP".equals(formCode)) { - repPage = SLAPP_REP_PAGE; - } else if ("SLGII".equals(formCode)) { - repPage = SLGII_REP_PAGE; - } - // Multi-Page Replacement HSBCA31 - else if ("HSBCA31".equals(formCode) && totalPagesA >= HSBCA31_REP_END) { - isMultiPageReplace = true; - repStartA = HSBCA31_REP_START; - repEndA = HSBCA31_REP_END; - repCountB = HSBCA31_REP_COUNT; + + if (configOpt.isEmpty()) { + merger.merge(docA, 1, totalPagesA); + mergedPdf.close(); + return baos.toByteArray(); } - if ((repPage != -1 && totalPagesA >= repPage) || isMultiPageReplace) { - - // Determine start/end pages for copy segments - int firstSegmentEnd = isMultiPageReplace ? repStartA - 1 : repPage - 1; - int secondSegmentStart = isMultiPageReplace ? repEndA + 1 : repPage + 1; - int pagesToInsertB = isMultiPageReplace ? repCountB : 1; + FormSigPage cfg = configOpt.get(); + int pageFrom = cfg.getPageFrom(); + int pageTo = cfg.getPageTo(); + boolean skipAndAppend = SKIP_AND_APPEND.equals(cfg.getAction()); - // A. Copy pages 1 up to the start of replacement - if (firstSegmentEnd >= 1) { - merger.merge(docA, 1, firstSegmentEnd); + if (skipAndAppend && totalPagesA >= pageFrom) { + if (pageFrom > 1) { + merger.merge(docA, 1, pageFrom - 1); + } + if (totalPagesA > pageFrom) { + merger.merge(docA, pageFrom + 1, totalPagesA); } - - // B. Insert replacement pages from PDF B (if available) if (pdfBFlattenedBytes != null) { try (PdfReader readerB_merge = new PdfReader(new ByteArrayInputStream(pdfBFlattenedBytes)); PdfDocument docB = new PdfDocument(readerB_merge)) { - - if (docB.getNumberOfPages() >= pagesToInsertB) { - merger.merge(docB, 1, pagesToInsertB); - } + merger.merge(docB, 1, 1); } } - - // C. Copy pages after replacement through the end - if (totalPagesA >= secondSegmentStart) { - merger.merge(docA, secondSegmentStart, totalPagesA); - } - - // --- Existing Logic (Skip Pages) --- - } else if ("IDA".equals(formCode) && totalPagesA >= IDA_SIG_PAGE) { - // IDA: SKIP page 15 - if (IDA_SIG_PAGE > 1) { - merger.merge(docA, 1, IDA_SIG_PAGE - 1); - } - if (totalPagesA > IDA_SIG_PAGE) { - merger.merge(docA, IDA_SIG_PAGE + 1, totalPagesA); - } - - } else if ("FNA".equals(formCode) && totalPagesA >= FNA_SIG_PAGE) { - // FNA: SKIP page 10 - if (FNA_SIG_PAGE > 1) { - merger.merge(docA, 1, FNA_SIG_PAGE - 1); - } - if (totalPagesA > FNA_SIG_PAGE) { - merger.merge(docA, FNA_SIG_PAGE + 1, totalPagesA); - } - - } else if ("HSBCFIN".equals(formCode) && totalPagesA >= HSBC_REP_PAGE) { - // HSBCFIN: REPLACE page 11 with PDF B page 1 - - if (HSBC_REP_PAGE > 1) { - merger.merge(docA, 1, HSBC_REP_PAGE - 1); + } else if (!skipAndAppend && totalPagesA >= pageTo) { + int repStartA = pageFrom; + int repEndA = pageTo; + int pagesToInsertB = pageTo - pageFrom + 1; + if (repStartA > 1) { + merger.merge(docA, 1, repStartA - 1); } - if (pdfBFlattenedBytes != null) { try (PdfReader readerB_merge = new PdfReader(new ByteArrayInputStream(pdfBFlattenedBytes)); - PdfDocument docB = new PdfDocument(readerB_merge)) { - merger.merge(docB, 1, 1); + PdfDocument docB = new PdfDocument(readerB_merge)) { + if (docB.getNumberOfPages() >= pagesToInsertB) { + merger.merge(docB, 1, pagesToInsertB); + } } } - - if (totalPagesA > HSBC_REP_PAGE) { - merger.merge(docA, HSBC_REP_PAGE + 1, totalPagesA); + if (totalPagesA > repEndA) { + merger.merge(docA, repEndA + 1, totalPagesA); } - } else { - // Default: Copy all pages from docA merger.merge(docA, 1, totalPagesA); } - - // 4. Process PDF B (Only appended if IDA or FNA) - if (pdfBFlattenedBytes != null) { - if ("IDA".equals(formCode) || "FNA".equals(formCode)){ - try (PdfReader readerB_merge = new PdfReader(new ByteArrayInputStream(pdfBFlattenedBytes)); - PdfDocument docB = new PdfDocument(readerB_merge)) { - - // Copy ONLY page 1 from docB to append as the last page - merger.merge(docB, 1, 1); - } - } - } - mergedPdf.close(); + mergedPdf.close(); return baos.toByteArray(); } } - public byte[] mergePdf2sItext7(String formCode, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + /** Backward compatibility: use today as asOfDate. */ + public byte[] mergePdfsItext7(String formCode, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + return mergePdfsItext7(formCode, LocalDate.now(), pdfABytes, pdfBBytes); + } + + /** + * Merge upload2 PDF using config from form_sig_page. + */ + public byte[] mergePdf2sItext7(String formCode, LocalDate asOfDate, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + Optional configOpt = formSigPageService.getUpload2Config(formCode, asOfDate); - // UPDATED CONSTANTS FOR NEW REQUIREMENTS - final int MLB03S_REP_START_A = 12; - final int MLB03S_REP_END_A = 13; - final int MLB03S_REP_COUNT_B = 2; - final int SLAPP_REP_START_A = 19; - final int SLAPP_REP_END_A = 20; - final int SLAPP_REP_COUNT_B = 2; - final int SLGII_REP_START_A = 15; - final int SLGII_REP_END_A = 16; - final int SLGII_REP_COUNT_B = 2; - - // --- STEP 1: Flatten PDF A and get the modified bytes --- byte[] pdfAFlattenedBytes; try (PdfReader readerA = new PdfReader(new ByteArrayInputStream(pdfABytes)); ByteArrayOutputStream tempBaosA = new ByteArrayOutputStream(); PdfWriter tempWriterA = new PdfWriter(tempBaosA); PdfDocument docA_mod = new PdfDocument(readerA, tempWriterA)) { - + flattenPdf(docA_mod); - docA_mod.close(); // IMPORTANT: Close to finalize writing to tempBaosA + docA_mod.close(); pdfAFlattenedBytes = tempBaosA.toByteArray(); } - // --- STEP 2: Flatten PDF B and get the modified bytes (if needed) --- byte[] pdfBFlattenedBytes = null; if (pdfBBytes != null && pdfBBytes.length > 0) { try (PdfReader readerB = new PdfReader(new ByteArrayInputStream(pdfBBytes)); ByteArrayOutputStream tempBaosB = new ByteArrayOutputStream(); PdfWriter tempWriterB = new PdfWriter(tempBaosB); PdfDocument docB_mod = new PdfDocument(readerB, tempWriterB)) { - + flattenPdf(docB_mod); - docB_mod.close(); // IMPORTANT: Close to finalize writing to tempBaosB + docB_mod.close(); pdfBFlattenedBytes = tempBaosB.toByteArray(); } } - - // --- STEP 3: Perform the merge using the flattened bytes --- - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter writer = new PdfWriter(baos); PdfDocument mergedPdf = new PdfDocument(writer); PdfReader readerA_merge = new PdfReader(new ByteArrayInputStream(pdfAFlattenedBytes)); - PdfDocument docA = new PdfDocument(readerA_merge)) { + PdfDocument docA = new PdfDocument(readerA_merge)) { - // ⭐️ FIX: PdfMerger is NOT AutoCloseable, so we instantiate it here. PdfMerger merger = new PdfMerger(mergedPdf); - int totalPagesA = docA.getNumberOfPages(); - - // --- Multi-page Replacement Forms (SLAPP, SLGII) --- - int repStartA = -1; - int repEndA = -1; - int repCountB = -1; - - if ("SLAPP".equals(formCode)) { - repStartA = SLAPP_REP_START_A; - repEndA = SLAPP_REP_END_A; - repCountB = SLAPP_REP_COUNT_B; - } else if ("SLGII".equals(formCode)) { - repStartA = SLGII_REP_START_A; - repEndA = SLGII_REP_END_A; - repCountB = SLGII_REP_COUNT_B; - } else if ("MLB03S".equals(formCode)) { - repStartA = MLB03S_REP_START_A; - repEndA = MLB03S_REP_END_A; - repCountB = MLB03S_REP_COUNT_B; + + if (configOpt.isEmpty()) { + merger.merge(docA, 1, totalPagesA); + mergedPdf.close(); + return baos.toByteArray(); } - if (repStartA != -1 && totalPagesA >= repEndA) { - - // A. Copy pages 1 up to (repStartA - 1) + FormSigPage cfg = configOpt.get(); + int repStartA = cfg.getPageFrom(); + int repEndA = cfg.getPageTo(); + int repCountB = repEndA - repStartA + 1; + + if (totalPagesA >= repEndA) { if (repStartA > 1) { merger.merge(docA, 1, repStartA - 1); } - - // B. Insert replacement pages from PDF B (if available) if (pdfBFlattenedBytes != null) { try (PdfReader readerB_merge = new PdfReader(new ByteArrayInputStream(pdfBFlattenedBytes)); PdfDocument docB = new PdfDocument(readerB_merge)) { - - // Copy the required number of pages from docB starting from page 1 if (docB.getNumberOfPages() >= repCountB) { merger.merge(docB, 1, repCountB); - } else { - System.err.println("PDF B too short for " + formCode + " replacement."); } } } - - // C. Copy pages (repEndA + 1) through the end if (totalPagesA > repEndA) { merger.merge(docA, repEndA + 1, totalPagesA); } - - // --- Single Page Replacement Forms (MLB03S) --- - /* - } else if ("MLB03S".equals(formCode) && totalPagesA >= MLB03S_REP_PAGE_A) { - - // A. Copy pages 1 up to 11 (MLB03S_REP_PAGE_A - 1) - if (MLB03S_REP_PAGE_A > 1) { - merger.merge(docA, 1, MLB03S_REP_PAGE_A - 1); - } - - // B. Insert replacement page from PDF B (if available) - if (pdfBFlattenedBytes != null) { - try (PdfReader readerB_merge = new PdfReader(new ByteArrayInputStream(pdfBFlattenedBytes)); - PdfDocument docB = new PdfDocument(readerB_merge)) { - - // Copy ONLY page 1 from docB - if (docB.getNumberOfPages() >= 1) { - merger.merge(docB, 1, 1); - } - } - } - - // C. Copy pages 13 through the end (MLB03S_REP_PAGE_A + 1) - if (totalPagesA > MLB03S_REP_PAGE_A) { - merger.merge(docA, MLB03S_REP_PAGE_A + 1, totalPagesA); - } - */ } else { - // Default: Copy all pages from docA merger.merge(docA, 1, totalPagesA); } - - mergedPdf.close(); - return baos.toByteArray(); + mergedPdf.close(); + return baos.toByteArray(); } } -} \ No newline at end of file + + /** Backward compatibility: use today as asOfDate. */ + public byte[] mergePdf2sItext7(String formCode, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { + return mergePdf2sItext7(formCode, LocalDate.now(), pdfABytes, pdfBBytes); + } +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java index c93b057..921951b 100644 --- a/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java @@ -4696,6 +4696,7 @@ public class PdfService extends AbstractBaseEntityService list( + @RequestParam(required = false) String formCode, + @RequestParam(required = false) String sigType) { + List records = formSigPageService.list(formCode, sigType); + return new RecordsRes<>(records); + } + + @GetMapping("/{id}") + public Map get(@PathVariable Long id) { + return Map.of( + Params.DATA, + formSigPageService.find(id).orElseThrow(NotFoundException::new)); + } + + @PostMapping("/save") + public IdRes save(@RequestBody @Valid UpdateFormSigPageReq req) { + FormSigPage saved = formSigPageService.save(req); + return new IdRes(saved.getId()); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long id) { + formSigPageService.markDelete(id); + } +} diff --git a/src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java b/src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java index a45dc97..c294399 100644 --- a/src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java +++ b/src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java @@ -1,10 +1,16 @@ package com.ffii.lioner.modules.lioner.pdf.web; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.sql.Timestamp; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.pdmodel.PDDocument; @@ -31,6 +37,7 @@ import com.ffii.core.utils.CriteriaArgsBuilder; import com.ffii.core.utils.Params; import com.ffii.core.utils.StringUtils; import com.ffii.lioner.modules.lioner.entity.FileBlob; +import com.ffii.lioner.modules.lioner.pdf.service.FormSigPageService; import com.ffii.lioner.modules.lioner.pdf.service.PdfMergeService; import com.ffii.lioner.modules.lioner.pdf.service.PdfService; import com.ffii.lioner.modules.lioner.service.FileService; @@ -50,6 +57,9 @@ public class PdfController { @Autowired private PdfMergeService pdfMergeService; + @Autowired + private FormSigPageService formSigPageService; + @Autowired private UserActionLogService userActionLogService; @@ -286,17 +296,20 @@ public class PdfController { } // --- 2. PDF Merging and Processing --- - byte[] finalPdfBytes = new byte[0]; - + LocalDate asOfDate = toLocalDate(d.get("created")); + if (asOfDate == null) { + asOfDate = LocalDate.now(); + } + byte[] finalPdfBytes; logger.info("pdfBytes:" + pdfBytes.length); - logger.info("pdfUpload1Bytes:" + pdfUpload1Bytes.length); + logger.info("pdfUpload1Bytes:" + (pdfUpload1Bytes != null ? pdfUpload1Bytes.length : 0)); logger.info("formCode:" + formCode); - finalPdfBytes = pdfMergeService.mergePdfsItext7(formCode, pdfBytes, pdfUpload1Bytes); + finalPdfBytes = pdfMergeService.mergePdfsItext7(formCode, asOfDate, pdfBytes, pdfUpload1Bytes); - //Forms that need to have 2nd upload sig - if(d.get("upload2FileId") != null) - finalPdfBytes = pdfMergeService.mergePdf2sItext7(formCode, finalPdfBytes, pdfUpload2Bytes); + if (d.get("upload2FileId") != null) { + finalPdfBytes = pdfMergeService.mergePdf2sItext7(formCode, asOfDate, finalPdfBytes, pdfUpload2Bytes); + } // --- 4. Build ResponseEntity --- HttpHeaders headers = new HttpHeaders(); @@ -312,6 +325,40 @@ public class PdfController { return new ResponseEntity<>(finalPdfBytes, headers, HttpStatus.OK); } + /** + * Returns form_sig_page config for labels (upload1/upload2 page range and label text). + * Optional asOfDate (yyyy-MM-dd); defaults to today if omitted. + */ + @GetMapping("/form-sig-page-config") + public Map getFormSigPageConfig( + @RequestParam String formCode, + @RequestParam(required = false) String asOfDate) { + LocalDate date = parseLocalDate(asOfDate); + if (date == null) { + date = LocalDate.now(); + } + List> configs = formSigPageService.getConfigDtosForForm(formCode, date); + return Map.of("formCode", formCode, "asOfDate", date.toString(), "configs", configs); + } + + private static LocalDate toLocalDate(Object o) { + if (o == null) return null; + if (o instanceof LocalDate) return (LocalDate) o; + if (o instanceof LocalDateTime) return ((LocalDateTime) o).toLocalDate(); + if (o instanceof Timestamp) return ((Timestamp) o).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + if (o instanceof java.util.Date) return ((java.util.Date) o).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return null; + } + + private static LocalDate parseLocalDate(String s) { + if (s == null || s.isBlank()) return null; + try { + return LocalDate.parse(s); + } catch (Exception e) { + return null; + } + } + /* @GetMapping(value = "/download-ff", produces = MediaType.APPLICATION_PDF_VALUE) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 95e7c0d..6ef49cf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,7 +15,7 @@ spring: datasource: jdbc-url: jdbc:mysql://localhost:3306/lionerdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 username: root - password: cFDp7988vc+$] + password: secret servlet: multipart: diff --git a/src/main/resources/db/changelog/changes/38_form_sig_page/01_add_form_sig_page_table.sql b/src/main/resources/db/changelog/changes/38_form_sig_page/01_add_form_sig_page_table.sql new file mode 100644 index 0000000..5348c43 --- /dev/null +++ b/src/main/resources/db/changelog/changes/38_form_sig_page/01_add_form_sig_page_table.sql @@ -0,0 +1,23 @@ +--liquibase formatted sql + +--changeset lioner:add form_sig_page table + +CREATE TABLE `form_sig_page` ( + `id` int NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` int DEFAULT NULL, + `version` int NOT NULL DEFAULT '0', + `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modifiedBy` int DEFAULT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT '0', + + `formCode` varchar(50) NOT NULL, + `startDate` date NOT NULL, + `sigType` varchar(20) NOT NULL, + `pageFrom` int NOT NULL, + `pageTo` int NOT NULL, + `action` varchar(30) NOT NULL DEFAULT 'REPLACE', + + PRIMARY KEY (`id`), + KEY `idx_form_sig_page_form_start` (`formCode`, `startDate`, `sigType`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/main/resources/db/changelog/changes/38_form_sig_page/02_insert_form_sig_page_data.sql b/src/main/resources/db/changelog/changes/38_form_sig_page/02_insert_form_sig_page_data.sql new file mode 100644 index 0000000..8d1b28b --- /dev/null +++ b/src/main/resources/db/changelog/changes/38_form_sig_page/02_insert_form_sig_page_data.sql @@ -0,0 +1,19 @@ +--liquibase formatted sql + +--changeset lioner:insert form_sig_page seed data + +INSERT INTO `form_sig_page` (`formCode`, `startDate`, `sigType`, `pageFrom`, `pageTo`, `action`) VALUES +('IDA', '2000-01-01', 'upload1', 15, 15, 'SKIP_AND_APPEND'), +('FNA', '2000-01-01', 'upload1', 10, 10, 'SKIP_AND_APPEND'), +('HSBCFIN', '2000-01-01', 'upload1', 11, 11, 'REPLACE'), +('HSBCA31', '2000-01-01', 'upload1', 28, 29, 'REPLACE'), +('MLB03S', '2000-01-01', 'upload1', 9, 9, 'REPLACE'), +('MLFNA_EN', '2000-01-01', 'upload1', 4, 4, 'REPLACE'), +('MLFNA_CHI', '2000-01-01', 'upload1', 4, 4, 'REPLACE'), +('SLFNA_EN', '2000-01-01', 'upload1', 5, 5, 'REPLACE'), +('SLFNA_CHI', '2000-01-01', 'upload1', 5, 5, 'REPLACE'), +('SLAPP', '2000-01-01', 'upload1', 17, 17, 'REPLACE'), +('SLGII', '2000-01-01', 'upload1', 13, 13, 'REPLACE'), +('MLB03S', '2000-01-01', 'upload2', 12, 13, 'REPLACE'), +('SLAPP', '2000-01-01', 'upload2', 19, 20, 'REPLACE'), +('SLGII', '2000-01-01', 'upload2', 15, 16, 'REPLACE');