Browse Source

added dynamic page no. for formCode

master
PC-20260115JRSN\Administrator 5 days ago
parent
commit
1be2fbece3
11 changed files with 597 additions and 240 deletions
  1. +88
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPage.java
  2. +14
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPageRepository.java
  3. +89
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/req/UpdateFormSigPageReq.java
  4. +149
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/service/FormSigPageService.java
  5. +91
    -232
      src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java
  6. +1
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java
  7. +68
    -0
      src/main/java/com/ffii/lioner/modules/lioner/pdf/web/FormSigPageController.java
  8. +54
    -7
      src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java
  9. +1
    -1
      src/main/resources/application.yml
  10. +23
    -0
      src/main/resources/db/changelog/changes/38_form_sig_page/01_add_form_sig_page_table.sql
  11. +19
    -0
      src/main/resources/db/changelog/changes/38_form_sig_page/02_insert_form_sig_page_data.sql

+ 88
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPage.java View File

@@ -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;
}
}

+ 14
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/entity/FormSigPageRepository.java View File

@@ -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();
}

+ 89
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/req/UpdateFormSigPageReq.java View File

@@ -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;
}
}

+ 149
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/service/FormSigPageService.java View File

@@ -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;
}
}

+ 91
- 232
src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfMergeService.java View File

@@ -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);
}
}

+ 1
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/service/PdfService.java View File

@@ -4696,6 +4696,7 @@ public class PdfService extends AbstractBaseEntityService<Pdf, Long, PdfReposito
+ " ff.id, "
+ " ff.remarks, "
+ " ff.created, "
+ " f.filename, "
+ " c.clientCode, "
+ " c.lastname, "


+ 68
- 0
src/main/java/com/ffii/lioner/modules/lioner/pdf/web/FormSigPageController.java View File

@@ -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);
}
}

+ 54
- 7
src/main/java/com/ffii/lioner/modules/lioner/pdf/web/PdfController.java View File

@@ -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)


+ 1
- 1
src/main/resources/application.yml View File

@@ -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:


+ 23
- 0
src/main/resources/db/changelog/changes/38_form_sig_page/01_add_form_sig_page_table.sql View File

@@ -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;

+ 19
- 0
src/main/resources/db/changelog/changes/38_form_sig_page/02_insert_form_sig_page_data.sql View File

@@ -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');

Loading…
Cancel
Save