| @@ -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<Long> { | |||
| @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; | |||
| } | |||
| } | |||
| @@ -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<FormSigPage, Long> { | |||
| List<FormSigPage> findByFormCodeAndSigTypeAndStartDateLessThanEqualAndDeletedFalseOrderByStartDateDesc( | |||
| String formCode, String sigType, LocalDate asOfDate); | |||
| List<FormSigPage> findByDeletedFalseOrderByFormCodeAscStartDateDesc(); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<FormSigPage> list(String formCode, String sigType) { | |||
| List<FormSigPage> 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<FormSigPage> 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<FormSigPage> getConfig(String formCode, LocalDate asOfDate, String sigType) { | |||
| if (formCode == null || asOfDate == null || sigType == null) { | |||
| return Optional.empty(); | |||
| } | |||
| List<FormSigPage> list = formSigPageRepository | |||
| .findByFormCodeAndSigTypeAndStartDateLessThanEqualAndDeletedFalseOrderByStartDateDesc( | |||
| formCode, sigType, asOfDate); | |||
| return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); | |||
| } | |||
| public Optional<FormSigPage> getUpload1Config(String formCode, LocalDate asOfDate) { | |||
| return getConfig(formCode, asOfDate, UPLOAD1); | |||
| } | |||
| public Optional<FormSigPage> getUpload2Config(String formCode, LocalDate asOfDate) { | |||
| return getConfig(formCode, asOfDate, UPLOAD2); | |||
| } | |||
| /** | |||
| * Converts LocalDateTime (e.g. filled_form.created) to LocalDate for lookup. | |||
| */ | |||
| public Optional<FormSigPage> getUpload1Config(String formCode, LocalDateTime asOf) { | |||
| return asOf == null ? Optional.empty() : getUpload1Config(formCode, asOf.toLocalDate()); | |||
| } | |||
| public Optional<FormSigPage> 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<String, FormSigPage> getConfigForForm(String formCode, LocalDate asOfDate) { | |||
| Map<String, FormSigPage> 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<String, FormSigPage> 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<Map<String, Object>> getConfigDtosForForm(String formCode, LocalDate asOfDate) { | |||
| return getConfigForForm(formCode, asOfDate).values().stream() | |||
| .map(this::toDto) | |||
| .collect(Collectors.toList()); | |||
| } | |||
| public List<Map<String, Object>> getConfigDtosForForm(String formCode, LocalDateTime asOf) { | |||
| return asOf == null ? List.of() : getConfigDtosForForm(formCode, asOf.toLocalDate()); | |||
| } | |||
| private Map<String, Object> toDto(FormSigPage c) { | |||
| Map<String, Object> 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; | |||
| } | |||
| } | |||
| @@ -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<FormSigPage> 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<FormSigPage> 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(); | |||
| } | |||
| } | |||
| } | |||
| /** Backward compatibility: use today as asOfDate. */ | |||
| public byte[] mergePdf2sItext7(String formCode, byte[] pdfABytes, byte[] pdfBBytes) throws IOException { | |||
| return mergePdf2sItext7(formCode, LocalDate.now(), pdfABytes, pdfBBytes); | |||
| } | |||
| } | |||
| @@ -4696,6 +4696,7 @@ public class PdfService extends AbstractBaseEntityService<Pdf, Long, PdfReposito | |||
| + " ff.id, " | |||
| + " ff.remarks, " | |||
| + " ff.created, " | |||
| + " f.filename, " | |||
| + " c.clientCode, " | |||
| + " c.lastname, " | |||
| @@ -0,0 +1,68 @@ | |||
| package com.ffii.lioner.modules.lioner.pdf.web; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import org.springframework.http.HttpStatus; | |||
| import org.springframework.security.access.prepost.PreAuthorize; | |||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.PathVariable; | |||
| import org.springframework.web.bind.annotation.PostMapping; | |||
| import org.springframework.web.bind.annotation.RequestBody; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RequestParam; | |||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| import com.ffii.core.exception.NotFoundException; | |||
| import com.ffii.core.response.IdRes; | |||
| import com.ffii.core.response.RecordsRes; | |||
| import com.ffii.core.utils.Params; | |||
| import com.ffii.lioner.modules.lioner.pdf.entity.FormSigPage; | |||
| import com.ffii.lioner.modules.lioner.pdf.req.UpdateFormSigPageReq; | |||
| import com.ffii.lioner.modules.lioner.pdf.service.FormSigPageService; | |||
| import jakarta.validation.Valid; | |||
| /** | |||
| * Admin-only CRUD for form_sig_page (signature page config per formCode/startDate/sigType). | |||
| */ | |||
| @RestController | |||
| @RequestMapping("/pdf/form-sig-page") | |||
| @PreAuthorize("hasAuthority('MANAGE_SYSTEM_CONFIGURATION')") | |||
| public class FormSigPageController { | |||
| private final FormSigPageService formSigPageService; | |||
| public FormSigPageController(FormSigPageService formSigPageService) { | |||
| this.formSigPageService = formSigPageService; | |||
| } | |||
| @GetMapping | |||
| public RecordsRes<FormSigPage> list( | |||
| @RequestParam(required = false) String formCode, | |||
| @RequestParam(required = false) String sigType) { | |||
| List<FormSigPage> records = formSigPageService.list(formCode, sigType); | |||
| return new RecordsRes<>(records); | |||
| } | |||
| @GetMapping("/{id}") | |||
| public Map<String, Object> 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); | |||
| } | |||
| } | |||
| @@ -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<String, Object> getFormSigPageConfig( | |||
| @RequestParam String formCode, | |||
| @RequestParam(required = false) String asOfDate) { | |||
| LocalDate date = parseLocalDate(asOfDate); | |||
| if (date == null) { | |||
| date = LocalDate.now(); | |||
| } | |||
| List<Map<String, Object>> 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) | |||
| @@ -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: | |||
| @@ -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; | |||
| @@ -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'); | |||