package com.emergencyrepair.business.service;
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.emergencyrepair.business.entity.CompletionInfo;
|
import com.emergencyrepair.business.entity.Fault;
|
import com.emergencyrepair.business.entity.WorkOrder;
|
import com.emergencyrepair.business.mapper.CompletionInfoMapper;
|
import com.emergencyrepair.business.mapper.FaultMapper;
|
import com.emergencyrepair.business.mapper.WorkOrderMapper;
|
import com.emergencyrepair.business.entity.ProductNode;
|
import com.emergencyrepair.business.mapper.ProductNodeMapper;
|
import com.emergencyrepair.common.BusinessException;
|
import lombok.RequiredArgsConstructor;
|
import org.apache.poi.ooxml.POIXMLProperties;
|
import org.apache.poi.wp.usermodel.HeaderFooterType;
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
import org.apache.poi.xwpf.usermodel.XWPFFooter;
|
import org.apache.poi.xwpf.usermodel.XWPFHeader;
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
import org.apache.xmlbeans.XmlObject;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STYAlign;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTextDirection;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVAnchor;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import javax.annotation.PostConstruct;
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.math.BigInteger;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.nio.file.Paths;
|
import java.time.LocalDate;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
/**
|
* 工程单打印(需求 32)。
|
*
|
* <p>把选中的故障记录按 Word 模板输出成 A4 横向的四联工程单,明细可自动分页。
|
* 模板里用 {@code {字段名}} 占位,渲染时替换成真实数据。</p>
|
*
|
* <p>模板文件默认放在 {@code config/engineering-order-template.docx},首次启动若不存在会自动生成一份,
|
* 运维/业务人员可以直接用 Word 打开修改标题、列宽、联头文字,不需要改代码。</p>
|
*
|
* <p>业务配置放在 {@code config/business-config.yml},可按联号维护第 1~4 联名称。</p>
|
*
|
* <p>模板约定(包含明细字段的表格):</p>
|
* <ul>
|
* <li>明细表最后一行是数据行模板,会按选中记录数复制展开,记录行不允许跨页拆分;</li>
|
* <li>联级字段可以用 {@code {xNo}}、{@code {teamMajor}}、{@code {maxFinishDate}}、{@code {unitNo}}、{@code {unitName}}、{@code {unitDisplay}};</li>
|
* <li>整份模板会被复制 4 次输出为四联(每联一页),{@code {unitNo}} 依次为 1/2/3/4。</li>
|
* </ul>
|
*/
|
@Service
|
@RequiredArgsConstructor
|
public class EngineeringOrderPrintService {
|
private static final Logger log = LoggerFactory.getLogger(EngineeringOrderPrintService.class);
|
|
/** 一个 Word 文件里的联数。 */
|
private static final int UNIT_COUNT = 4;
|
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private static final Pattern PLACEHOLDER = Pattern.compile("\\{([^{}]*)\\}");
|
private static final String TEMPLATE_VERSION = "engineering-order-template-v4";
|
private static final Pattern TEMPLATE_PLACEHOLDER =
|
Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*\\}\\}|\\{\\s*([^{}]+?)\\s*\\}");
|
|
/** 只有包含明细字段的表格才按记录数展开最后一行。 */
|
private static final Set<String> DETAIL_PLACEHOLDERS = new HashSet<>(Arrays.asList(
|
"faultCode", "engineeringNo", "workOrderNo", "equipmentName", "quantity", "equipmentCount",
|
"parentSystem", "engineeringMajor", "faultImpact", "faultNature", "majorFaultDesc",
|
"repairRequirement", "engineeringNameDesc", "isSelfRepair", "isHRelated", "isMonitorItem",
|
"monitorItem", "monitorValue", "requestedFinishDate", "remark", "contractorName",
|
"contractorNames", "startDate", "finishDate", "owner", "reportUnit", "reportDate"
|
));
|
|
|
private static final BigInteger A4_LANDSCAPE_WIDTH = BigInteger.valueOf(16838);
|
private static final BigInteger A4_LANDSCAPE_HEIGHT = BigInteger.valueOf(11906);
|
private static final BigInteger PAGE_MARGIN = BigInteger.valueOf(567);
|
|
/** 默认模板的联头占位文字。 */
|
private static final String DEFAULT_UNIT_HEADER =
|
"X号:{xNo} ZB型号:{zbModel} 专业:{teamMajor} 最大要求完工时间:{maxFinishDate}";
|
|
/** 默认模板的列定义:标题 + 字段占位符。 */
|
private static final String[][] DEFAULT_COLUMNS = {
|
{"故障编号", "{faultCode}"},
|
{"设备名称", "{equipmentName}"},
|
{"工程名称及损坏情况", "{engineeringNameDesc}"},
|
{"修理技术要求", "{repairRequirement}"},
|
{"承修单位", "{contractorName}"},
|
{"数量", "{quantity}"},
|
{"备注", "{remark}"}
|
};
|
|
/** 默认模板的列宽(twips),合计约等于 A4 横向可用宽度。 */
|
private static final int UNIT_COLUMN_WIDTH = 400;
|
private static final int[] TABLE_COLUMN_WIDTHS = {1450, 2750, 3300, 3300, 2000, 800, 1700, UNIT_COLUMN_WIDTH};
|
|
private final FaultMapper faultMapper;
|
private final WorkOrderMapper workOrderMapper;
|
private final CompletionInfoMapper completionInfoMapper;
|
private final ProductNodeMapper productNodeMapper;
|
|
@Value("${app.engineering-print.template-path:${app.engineering-print.template:config/engineering-order-template.docx}}")
|
private String templateLocation;
|
|
@Value("${app.engineering-print.unit-names.1:上报人}")
|
private String unitName1;
|
|
@Value("${app.engineering-print.unit-names.2:}")
|
private String unitName2;
|
|
@Value("${app.engineering-print.unit-names.3:}")
|
private String unitName3;
|
|
@Value("${app.engineering-print.unit-names.4:}")
|
private String unitName4;
|
|
@PostConstruct
|
public void ensureTemplate() {
|
Path path = templatePath();
|
if (path == null) {
|
return;
|
}
|
try {
|
Files.createDirectories(path.getParent());
|
if (Files.exists(path)) {
|
return;
|
}
|
try (XWPFDocument document = buildDefaultTemplate();
|
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
document.write(out);
|
Files.write(path, out.toByteArray());
|
}
|
log.info("工程单打印模板已生成或升级:{}", path.toAbsolutePath());
|
} catch (Exception exception) {
|
log.warn("生成或升级工程单打印模板失败:{}", exception.getMessage());
|
}
|
}
|
|
private Map<Integer, String> loadUnitNames() {
|
Map<Integer, String> names = new LinkedHashMap<>();
|
names.put(1, text(unitName1).trim());
|
names.put(2, text(unitName2).trim());
|
names.put(3, text(unitName3).trim());
|
names.put(4, text(unitName4).trim());
|
return names;
|
}
|
|
private boolean templateNeedsUpgrade(Path path) {
|
try (InputStream input = Files.newInputStream(path);
|
XWPFDocument document = new XWPFDocument(input)) {
|
POIXMLProperties.CoreProperties properties = document.getProperties().getCoreProperties();
|
return !TEMPLATE_VERSION.equals(properties.getKeywords())
|
|| collectText(document).indexOf("{zbModel}") < 0
|
|| collectText(document).indexOf("{unitDisplay}") < 0;
|
} catch (Exception exception) {
|
log.warn("工程单打印模板无法读取,将重新生成:{}", exception.getMessage());
|
return true;
|
}
|
}
|
|
/**
|
* 按选中的故障记录渲染工程单 Word 文档。
|
*
|
* @param faultIds 故障主键
|
*/
|
public byte[] render(List<Long> faultIds) {
|
return renderInternal(faultIds, false);
|
}
|
|
/**
|
* 打印工程单2:按联分组输出,第1联全部明细后换页,再输出第2联,以此类推。
|
*/
|
public byte[] renderVersion2(List<Long> faultIds) {
|
return renderInternal(faultIds, true);
|
}
|
|
private byte[] renderInternal(List<Long> faultIds, boolean legacyGroupedUnits) {
|
List<Long> ids = normalizeIds(faultIds);
|
if (ids.isEmpty()) {
|
throw new BusinessException("请至少选择一条记录后再打印");
|
}
|
List<Map<String, String>> records = loadRecords(ids);
|
if (records.isEmpty()) {
|
throw new BusinessException("选中的记录已不存在,请刷新后重试");
|
}
|
Map<Integer, String> unitNames = loadUnitNames();
|
try (InputStream template = openTemplate();
|
XWPFDocument document = new XWPFDocument(template);
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
|
if (legacyGroupedUnits) {
|
renderLegacyGroupedUnits(document, records, unitNames);
|
} else {
|
renderUnits(document, records, unitNames);
|
}
|
document.write(buffer);
|
return buffer.toByteArray();
|
} catch (IOException exception) {
|
throw new BusinessException("生成工程单失败:" + exception.getMessage());
|
}
|
}
|
|
private List<Long> normalizeIds(List<Long> faultIds) {
|
if (faultIds == null) {
|
return new ArrayList<>();
|
}
|
List<Long> ids = new ArrayList<>();
|
for (Long id : faultIds) {
|
if (id != null && !ids.contains(id)) {
|
ids.add(id);
|
}
|
}
|
return ids;
|
}
|
|
private List<Map<String, String>> loadRecords(List<Long> ids) {
|
List<Fault> faults = faultMapper.selectBatchIds(ids);
|
Map<Long, Fault> faultById = new LinkedHashMap<>();
|
for (Fault fault : faults) {
|
faultById.put(fault.getId(), fault);
|
}
|
Map<String, String> zbModelByXNo = loadZbModels(faults);
|
|
List<WorkOrder> workOrders = workOrderMapper.selectList(Wrappers.<WorkOrder>lambdaQuery()
|
.in(WorkOrder::getFaultId, ids));
|
Map<Long, WorkOrder> workOrderByFault = new LinkedHashMap<>();
|
List<Long> workOrderIds = new ArrayList<>();
|
for (WorkOrder workOrder : workOrders) {
|
workOrderByFault.putIfAbsent(workOrder.getFaultId(), workOrder);
|
if (workOrder.getId() != null) {
|
workOrderIds.add(workOrder.getId());
|
}
|
}
|
|
Map<Long, CompletionInfo> completionByWorkOrder = new LinkedHashMap<>();
|
if (!workOrderIds.isEmpty()) {
|
List<CompletionInfo> completions = completionInfoMapper.selectList(Wrappers.<CompletionInfo>lambdaQuery()
|
.in(CompletionInfo::getWorkOrderId, workOrderIds));
|
for (CompletionInfo completion : completions) {
|
completionByWorkOrder.putIfAbsent(completion.getWorkOrderId(), completion);
|
}
|
}
|
|
LocalDate maxFinishDate = null;
|
for (CompletionInfo completion : completionByWorkOrder.values()) {
|
if (completion.getFinishDate() != null
|
&& (maxFinishDate == null || completion.getFinishDate().isAfter(maxFinishDate))) {
|
maxFinishDate = completion.getFinishDate();
|
}
|
}
|
if (maxFinishDate == null) {
|
for (Long id : ids) {
|
Fault fault = faultById.get(id);
|
if (fault != null && fault.getRequestedFinishDate() != null
|
&& (maxFinishDate == null || fault.getRequestedFinishDate().isAfter(maxFinishDate))) {
|
maxFinishDate = fault.getRequestedFinishDate();
|
}
|
}
|
}
|
|
List<Map<String, String>> records = new ArrayList<>();
|
for (Long id : ids) {
|
Fault fault = faultById.get(id);
|
if (fault == null) {
|
continue;
|
}
|
WorkOrder workOrder = workOrderByFault.get(id);
|
CompletionInfo completion = workOrder == null ? null : completionByWorkOrder.get(workOrder.getId());
|
String zbModel = zbModelByXNo.get(normalizeXNo(fault.getXNo()));
|
records.add(buildRecord(fault, workOrder, completion, maxFinishDate, zbModel));
|
}
|
return records;
|
}
|
|
private Map<String, String> loadZbModels(List<Fault> faults) {
|
Map<String, String> result = new LinkedHashMap<>();
|
List<String> xNos = new ArrayList<>();
|
for (Fault fault : faults) {
|
String xNo = normalizeXNo(fault.getXNo());
|
if (StringUtils.hasText(xNo) && !xNos.contains(xNo)) {
|
xNos.add(xNo);
|
}
|
}
|
if (xNos.isEmpty()) {
|
return result;
|
}
|
|
List<ProductNode> sideNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
|
.in(ProductNode::getLayer, java.util.Arrays.asList("side", "SIDE"))
|
.in(ProductNode::getName, xNos)
|
.ne(ProductNode::getIsDelete, "1")
|
.orderByAsc(ProductNode::getSortNo)
|
.orderByAsc(ProductNode::getId));
|
Map<String, Long> parentIdsByXNo = new LinkedHashMap<>();
|
for (ProductNode side : sideNodes) {
|
String xNo = normalizeXNo(side.getName());
|
if (StringUtils.hasText(xNo) && side.getParentId() != null && side.getParentId() != 0L) {
|
parentIdsByXNo.putIfAbsent(xNo, side.getParentId());
|
}
|
}
|
if (parentIdsByXNo.isEmpty()) {
|
return result;
|
}
|
|
List<Long> parentIds = new ArrayList<>(new java.util.LinkedHashSet<>(parentIdsByXNo.values()));
|
List<ProductNode> parents = productNodeMapper.selectBatchIds(parentIds);
|
Map<Long, String> parentNameById = new LinkedHashMap<>();
|
for (ProductNode parent : parents) {
|
if (!"1".equals(parent.getIsDelete())) {
|
parentNameById.put(parent.getId(), text(parent.getName()));
|
}
|
}
|
for (Map.Entry<String, Long> entry : parentIdsByXNo.entrySet()) {
|
result.put(entry.getKey(), text(parentNameById.get(entry.getValue())));
|
}
|
return result;
|
}
|
|
private String normalizeXNo(String value) {
|
return StringUtils.hasText(value) ? value.trim() : "";
|
}
|
|
private Map<String, String> buildRecord(Fault fault, WorkOrder workOrder, CompletionInfo completion,
|
LocalDate maxFinishDate, String zbModel) {
|
Map<String, String> values = new LinkedHashMap<>();
|
String contractor = firstText(
|
workOrder == null ? null : workOrder.getContractorName(),
|
completion == null ? null : completion.getContractorName(),
|
fault.getContractorNames());
|
String engineeringNo = firstText(
|
completion == null ? null : completion.getEngineeringNo(),
|
fault.getEngineeringNo());
|
|
values.put("faultCode", text(fault.getFaultCode()));
|
values.put("engineeringNo", text(engineeringNo));
|
values.put("workOrderNo", text(workOrder == null ? null : workOrder.getWorkOrderNo()));
|
values.put("equipmentName", text(fault.getEquipmentName()));
|
values.put("quantity", String.valueOf(equipmentQuantity(fault)));
|
values.put("equipmentCount", values.get("quantity"));
|
values.put("parentSystem", text(fault.getParentSystem()));
|
values.put("engineeringMajor", text(fault.getEngineeringMajor()));
|
values.put("teamMajor", text(fault.getTeamMajor()));
|
values.put("xNo", text(fault.getXNo()));
|
values.put("zbModel", text(zbModel));
|
values.put("owner", text(fault.getOwner()));
|
values.put("reportUnit", text(fault.getReportUnit()));
|
values.put("reportDate", date(fault.getReportDate()));
|
values.put("faultImpact", text(fault.getFaultImpact()));
|
values.put("faultNature", text(fault.getFaultNature()));
|
values.put("majorFaultDesc", text(fault.getMajorFaultDesc()));
|
values.put("repairRequirement", text(fault.getRepairRequirement()));
|
values.put("engineeringNameDesc", text(fault.getEngineeringNameDesc()));
|
values.put("isSelfRepair", yesNo(fault.getIsSelfRepair()));
|
values.put("isHRelated", yesNo(fault.getIsHRelated()));
|
values.put("isMonitorItem", yesNo(fault.getIsMonitorItem()));
|
values.put("monitorItem", text(fault.getMonitorItem()));
|
values.put("monitorValue", text(fault.getMonitorValue()));
|
values.put("requestedFinishDate", date(fault.getRequestedFinishDate()));
|
values.put("remark", text(fault.getRemark()));
|
values.put("contractorName", text(contractor));
|
values.put("contractorNames", text(fault.getContractorNames()));
|
values.put("startDate", date(completion == null ? null : completion.getStartDate()));
|
values.put("finishDate", date(completion == null ? null : completion.getFinishDate()));
|
values.put("maxFinishDate", date(maxFinishDate));
|
return values;
|
}
|
|
private int equipmentQuantity(Fault fault) {
|
String equipmentIds = fault.getEquipmentIds();
|
int count = 0;
|
if (StringUtils.hasText(equipmentIds)) {
|
for (String item : equipmentIds.split(",")) {
|
if (StringUtils.hasText(item)) {
|
count++;
|
}
|
}
|
}
|
if (count == 0 && fault.getEquipmentId() != null) {
|
count = 1;
|
}
|
return count;
|
}
|
|
private void renderUnits(XWPFDocument document,
|
List<Map<String, String>> records, Map<Integer, String> unitNames) {
|
CTBody body = document.getDocument().getBody();
|
List<XmlObject> templateElements = snapshotBodyElements(document);
|
if (templateElements.isEmpty()) {
|
throw new BusinessException("Engineering order template is empty: " + templateLocation);
|
}
|
|
int detailTableIndex = findDetailTableIndex(templateElements);
|
if (detailTableIndex < 0) {
|
throw new BusinessException("Engineering order template has no detail table: " + templateLocation);
|
}
|
int bottomTableIndex = findBottomTableIndex(templateElements, detailTableIndex);
|
Map<String, String> shared = new LinkedHashMap<>(records.get(0));
|
|
CTSectPr section = body.isSetSectPr() ? (CTSectPr) body.getSectPr().copy() : null;
|
CTBody cleanBody = CTBody.Factory.newInstance();
|
if (section != null) {
|
cleanBody.addNewSectPr().set(section);
|
}
|
body.set(cleanBody);
|
|
// Render each dynamically paginated detail page as four consecutive units.
|
|
renderPagedUnits(body, (CTTbl) templateElements.get(detailTableIndex), records, unitNames, section);
|
|
moveSectionPropertiesToEnd(body);
|
copyTemplatePrefixToHeader(document, templateElements, detailTableIndex, shared, records);
|
if (bottomTableIndex >= 0) {
|
copyTemplateTableToFooter(document, (CTTbl) templateElements.get(bottomTableIndex), shared, records);
|
}
|
}
|
|
private void renderPagedUnits(CTBody body, CTTbl templateTable,
|
List<Map<String, String>> records,
|
Map<Integer, String> unitNames,
|
CTSectPr section) {
|
List<List<Map<String, String>>> pages = paginateRecords(templateTable, records, section);
|
int renderedPage = 0;
|
for (List<Map<String, String>> page : pages) {
|
for (int unit = 1; unit <= UNIT_COUNT; unit++) {
|
String unitName = text(unitNames.get(unit)).trim();
|
Map<String, String> unitValues = new LinkedHashMap<>();
|
unitValues.put("unitNo", String.valueOf(unit));
|
unitValues.put("unitName", unitName);
|
unitValues.put("unitDisplay", "\u7b2c" + unit + "\u8054"
|
+ (StringUtils.hasText(unitName) ? " " + unitName : ""));
|
|
List<Map<String, String>> unitRecords = new ArrayList<>();
|
for (Map<String, String> record : page) {
|
Map<String, String> unitRecord = new LinkedHashMap<>(record);
|
unitRecord.putAll(unitValues);
|
unitRecords.add(unitRecord);
|
}
|
|
CTTbl table = body.addNewTbl();
|
table.set(templateTable);
|
clearTablePosition(table);
|
renderTable(table, unitValues, unitRecords);
|
int headerRowCount = table.sizeOfTrArray() - unitRecords.size();
|
configureUnitTablePagination(table, renderedPage > 0, headerRowCount);
|
configureUnitColumnVerticalMerge(table);
|
renderedPage++;
|
}
|
}
|
}
|
|
private List<List<Map<String, String>>> paginateRecords(CTTbl templateTable,
|
List<Map<String, String>> records,
|
CTSectPr section) {
|
List<List<Map<String, String>>> pages = new ArrayList<>();
|
if (records.isEmpty() || templateTable.sizeOfTrArray() == 0) {
|
return pages;
|
}
|
|
List<Integer> columnWidths = tableColumnWidths(templateTable);
|
int dataRowIndex = templateTable.sizeOfTrArray() - 1;
|
int headerHeight = 0;
|
for (int i = 0; i < dataRowIndex; i++) {
|
headerHeight += estimateRowHeight(templateTable.getTrArray(i), columnWidths, records.get(0));
|
}
|
|
int availableHeight = Math.max(1200,
|
(int) ((pageContentHeight(section) - headerHeight) * 0.50));
|
List<Map<String, String>> current = new ArrayList<>();
|
int usedHeight = 0;
|
for (Map<String, String> record : records) {
|
int rowHeight = estimateRowHeight(templateTable.getTrArray(dataRowIndex), columnWidths, record);
|
if (!current.isEmpty() && usedHeight + rowHeight > availableHeight) {
|
pages.add(current);
|
current = new ArrayList<>();
|
usedHeight = 0;
|
}
|
current.add(record);
|
usedHeight += rowHeight;
|
}
|
if (!current.isEmpty()) {
|
pages.add(current);
|
}
|
return pages;
|
}
|
|
private List<Integer> tableColumnWidths(CTTbl table) {
|
List<Integer> widths = new ArrayList<>();
|
CTTblGrid grid = table.getTblGrid();
|
if (grid != null) {
|
for (int i = 0; i < grid.sizeOfGridColArray(); i++) {
|
Object value = grid.getGridColArray(i).getW();
|
widths.add(value == null ? 0 : new BigInteger(String.valueOf(value)).intValue());
|
}
|
}
|
if (widths.isEmpty()) {
|
for (int width : TABLE_COLUMN_WIDTHS) {
|
widths.add(width);
|
}
|
}
|
return widths;
|
}
|
|
private int pageContentHeight(CTSectPr section) {
|
int height = A4_LANDSCAPE_HEIGHT.intValue();
|
int top = PAGE_MARGIN.intValue();
|
int bottom = PAGE_MARGIN.intValue();
|
if (section != null) {
|
if (section.isSetPgSz() && section.getPgSz().getH() != null) {
|
height = new BigInteger(String.valueOf(section.getPgSz().getH())).intValue();
|
}
|
if (section.isSetPgMar()) {
|
if (section.getPgMar().getTop() != null) {
|
top = new BigInteger(String.valueOf(section.getPgMar().getTop())).intValue();
|
}
|
if (section.getPgMar().getBottom() != null) {
|
bottom = new BigInteger(String.valueOf(section.getPgMar().getBottom())).intValue();
|
}
|
}
|
}
|
return Math.max(1200, height - top - bottom);
|
}
|
|
private int estimateRowHeight(CTRow row, List<Integer> columnWidths, Map<String, String> values) {
|
int maxHeight = 0;
|
int gridIndex = 0;
|
for (CTTc cell : row.getTcList()) {
|
int gridSpan = 1;
|
if (cell.isSetTcPr() && cell.getTcPr().isSetGridSpan()
|
&& cell.getTcPr().getGridSpan().getVal() != null) {
|
gridSpan = new BigInteger(String.valueOf(cell.getTcPr().getGridSpan().getVal())).intValue();
|
}
|
int cellWidth = cellWidthTwips(cell, columnWidths, gridIndex, gridSpan);
|
int cellHeight = 0;
|
for (CTP paragraph : cell.getPList()) {
|
String paragraphText = substitute(paragraphText(paragraph), values);
|
double fontSize = paragraphFontSize(paragraph, 9.0);
|
double usableWidth = Math.max(8.0, cellWidth / 20.0 - 2.0);
|
int lines = Math.max(1, (int) Math.ceil(textUnits(paragraphText) * fontSize / usableWidth));
|
cellHeight += (int) Math.ceil(lines * fontSize * 1.28 * 20.0) + 20;
|
}
|
maxHeight = Math.max(maxHeight, cellHeight);
|
gridIndex += gridSpan;
|
}
|
return Math.max(240, maxHeight);
|
}
|
|
private int cellWidthTwips(CTTc cell, List<Integer> columnWidths, int gridIndex, int gridSpan) {
|
if (cell.isSetTcPr() && cell.getTcPr().isSetTcW()) {
|
Object width = cell.getTcPr().getTcW().getW();
|
if (width != null && (!cell.getTcPr().getTcW().isSetType()
|
|| cell.getTcPr().getTcW().getType() == STTblWidth.DXA)) {
|
return new BigInteger(String.valueOf(width)).intValue();
|
}
|
}
|
int width = 0;
|
for (int i = gridIndex; i < gridIndex + gridSpan && i < columnWidths.size(); i++) {
|
width += columnWidths.get(i);
|
}
|
return width > 0 ? width : 1000;
|
}
|
|
private String paragraphText(CTP paragraph) {
|
StringBuilder builder = new StringBuilder();
|
for (CTR run : paragraph.getRList()) {
|
for (CTText text : run.getTList()) {
|
builder.append(text.getStringValue());
|
}
|
}
|
return builder.toString();
|
}
|
|
private double paragraphFontSize(CTP paragraph, double fallback) {
|
for (CTR run : paragraph.getRList()) {
|
if (run.isSetRPr() && run.getRPr().isSetSz() && run.getRPr().getSz().getVal() != null) {
|
return new BigInteger(String.valueOf(run.getRPr().getSz().getVal())).intValue() / 2.0;
|
}
|
}
|
return fallback;
|
}
|
|
private double textUnits(String value) {
|
double units = 0;
|
for (int i = 0; i < value.length(); i++) {
|
char character = value.charAt(i);
|
if (character == '\r' || character == '\n') {
|
continue;
|
}
|
if (character == ' ') {
|
units += 0.5;
|
} else if (character >= 0x2e80 || Character.isIdeographic(character)) {
|
units += 1.0;
|
} else {
|
units += 0.58;
|
}
|
}
|
return units;
|
}
|
|
private void renderInterleavedUnitTable(CTBody body, CTTbl templateTable,
|
List<Map<String, String>> records,
|
Map<Integer, String> unitNames) {
|
List<Map<String, String>> repeatedRecords = repeatRecordsByUnit(records, unitNames);
|
CTTbl table = body.addNewTbl();
|
table.set(templateTable);
|
clearTablePosition(table);
|
Map<String, String> headerValues = new LinkedHashMap<>(records.get(0));
|
headerValues.put("unitDisplay", "联号/联名");
|
renderTable(table, headerValues, repeatedRecords);
|
configureRecordRows(table, table.sizeOfTrArray() - repeatedRecords.size());
|
}
|
|
private List<Map<String, String>> repeatRecordsByUnit(List<Map<String, String>> records,
|
Map<Integer, String> unitNames) {
|
List<Map<String, String>> repeatedRecords = new ArrayList<>();
|
for (Map<String, String> record : records) {
|
for (int unit = 1; unit <= UNIT_COUNT; unit++) {
|
Map<String, String> values = new LinkedHashMap<>(record);
|
String unitName = text(unitNames.get(unit)).trim();
|
values.put("unitNo", String.valueOf(unit));
|
values.put("unitName", unitName);
|
values.put("unitDisplay", "第" + unit + "联"
|
+ (StringUtils.hasText(unitName) ? " " + unitName : ""));
|
repeatedRecords.add(values);
|
}
|
}
|
return repeatedRecords;
|
}
|
|
private void renderLegacyGroupedUnits(XWPFDocument document,
|
List<Map<String, String>> records,
|
Map<Integer, String> unitNames) {
|
CTBody body = document.getDocument().getBody();
|
List<XmlObject> templateElements = snapshotBodyElements(document);
|
if (templateElements.isEmpty()) {
|
throw new BusinessException("Engineering order template is empty: " + templateLocation);
|
}
|
|
int detailTableIndex = findDetailTableIndex(templateElements);
|
if (detailTableIndex < 0) {
|
throw new BusinessException("Engineering order template has no detail table: " + templateLocation);
|
}
|
int bottomTableIndex = findBottomTableIndex(templateElements, detailTableIndex);
|
|
Map<String, String> shared = new LinkedHashMap<>(records.get(0));
|
// The first and last template tables are page chrome. Put them into the
|
// Word header/footer so Word repeats them on every generated page.
|
copyTemplatePrefixToHeader(document, templateElements, detailTableIndex, shared, records);
|
if (bottomTableIndex >= 0) {
|
copyTemplateTableToFooter(document, (CTTbl) templateElements.get(bottomTableIndex), shared, records);
|
}
|
CTSectPr section = body.isSetSectPr() ? (CTSectPr) body.getSectPr().copy() : null;
|
CTBody cleanBody = CTBody.Factory.newInstance();
|
if (section != null) {
|
cleanBody.addNewSectPr().set(section);
|
}
|
body.set(cleanBody);
|
|
for (int unit = 1; unit <= UNIT_COUNT; unit++) {
|
if (unit > 1) {
|
appendPageBreakParagraph(body);
|
}
|
|
String unitName = text(unitNames.get(unit)).trim();
|
Map<String, String> unitValues = new LinkedHashMap<>();
|
unitValues.put("unitNo", String.valueOf(unit));
|
unitValues.put("unitName", unitName);
|
unitValues.put("unitDisplay", "第" + unit + "联"
|
+ (StringUtils.hasText(unitName) ? " " + unitName : ""));
|
|
Map<String, String> values = new LinkedHashMap<>(shared);
|
values.putAll(unitValues);
|
|
List<Map<String, String>> unitRecords = new ArrayList<>();
|
for (Map<String, String> record : records) {
|
Map<String, String> unitRecord = new LinkedHashMap<>(record);
|
unitRecord.putAll(unitValues);
|
unitRecords.add(unitRecord);
|
}
|
|
CTTbl table = body.addNewTbl();
|
table.set(templateElements.get(detailTableIndex));
|
clearTablePosition(table);
|
renderTable(table, values, unitRecords);
|
configureRecordRows(table, table.sizeOfTrArray() - unitRecords.size());
|
configureUnitColumnVerticalMerge(table);
|
}
|
|
moveSectionPropertiesToEnd(body);
|
}
|
|
private List<XmlObject> snapshotBodyElements(XWPFDocument document) {
|
List<XmlObject> elements = new ArrayList<>();
|
for (Object element : document.getBodyElements()) {
|
if (element instanceof XWPFParagraph) {
|
elements.add(((XWPFParagraph) element).getCTP().copy());
|
} else if (element instanceof XWPFTable) {
|
elements.add(((XWPFTable) element).getCTTbl().copy());
|
}
|
}
|
return elements;
|
}
|
|
private int findDetailTableIndex(List<XmlObject> elements) {
|
for (int i = 0; i < elements.size(); i++) {
|
XmlObject element = elements.get(i);
|
if (element instanceof CTTbl && containsDetailPlaceholder((CTTbl) element)) {
|
return i;
|
}
|
}
|
return -1;
|
}
|
|
private int findBottomTableIndex(List<XmlObject> elements, int detailTableIndex) {
|
int bottomTableIndex = -1;
|
for (int i = detailTableIndex + 1; i < elements.size(); i++) {
|
if (elements.get(i) instanceof CTTbl) {
|
bottomTableIndex = i;
|
}
|
}
|
return bottomTableIndex;
|
}
|
|
private void renderDetailTables(CTBody body, CTTbl table,
|
List<Map<String, String>> records,
|
Map<Integer, String> unitNames) {
|
int rowCount = table.sizeOfTrArray();
|
if (rowCount == 0) {
|
return;
|
}
|
|
int headerRowCount = rowCount - 1;
|
List<CTRow> headerRows = new ArrayList<>();
|
for (int i = 0; i < headerRowCount; i++) {
|
headerRows.add((CTRow) table.getTrArray(i).copy());
|
}
|
CTRow dataRow = (CTRow) table.getTrArray(rowCount - 1).copy();
|
|
CTTbl rowTemplate = (CTTbl) table.copy();
|
while (rowTemplate.sizeOfTrArray() > 0) {
|
rowTemplate.removeTr(0);
|
}
|
|
// Each unit is one continuously paginated table. Word fits as many detail
|
// rows per page as their actual rendered heights allow; cantSplit keeps each
|
// individual detail row intact across page boundaries.
|
for (int unit = 1; unit <= UNIT_COUNT; unit++) {
|
String unitName = text(unitNames.get(unit)).trim();
|
Map<String, String> unitValues = new LinkedHashMap<>();
|
unitValues.put("unitNo", String.valueOf(unit));
|
unitValues.put("unitName", unitName);
|
unitValues.put("unitDisplay", "\u7b2c" + unit + "\u8054"
|
+ (StringUtils.hasText(unitName) ? " " + unitName : ""));
|
|
CTTbl unitTable = (CTTbl) rowTemplate.copy();
|
for (CTRow headerRow : headerRows) {
|
CTRow renderedHeader = unitTable.addNewTr();
|
renderedHeader.set(headerRow);
|
replaceInRow(renderedHeader, unitValues);
|
}
|
|
for (Map<String, String> record : records) {
|
Map<String, String> values = new LinkedHashMap<>(record);
|
values.putAll(unitValues);
|
CTRow renderedData = unitTable.addNewTr();
|
renderedData.set(dataRow);
|
replaceInRow(renderedData, values);
|
}
|
|
configureUnitTablePagination(unitTable, unit > 1, headerRows.size());
|
body.addNewTbl().set(unitTable);
|
}
|
}
|
|
private void configureUnitTablePagination(CTTbl table, boolean pageBreakBefore, int headerRowCount) {
|
configureRecordRows(table, headerRowCount);
|
if (!pageBreakBefore || table.sizeOfTrArray() == 0) {
|
return;
|
}
|
CTRow firstRow = table.getTrArray(0);
|
if (firstRow.sizeOfTcArray() == 0) {
|
return;
|
}
|
CTTc firstCell = firstRow.getTcArray(0);
|
if (firstCell.sizeOfPArray() == 0) {
|
firstCell.addNewP();
|
}
|
setPageBreakBefore(firstCell.getPArray(0));
|
}
|
|
/** Merge the final unit-number column across every row in one unit table. */
|
private void configureUnitColumnVerticalMerge(CTTbl table) {
|
int rowCount = table.sizeOfTrArray();
|
for (int i = 0; i < rowCount; i++) {
|
CTRow row = table.getTrArray(i);
|
if (row.sizeOfTcArray() == 0) {
|
continue;
|
}
|
CTTc unitCell = row.getTcArray(row.sizeOfTcArray() - 1);
|
CTTcPr properties = unitCell.isSetTcPr() ? unitCell.getTcPr() : unitCell.addNewTcPr();
|
if (properties.isSetVMerge()) {
|
properties.unsetVMerge();
|
}
|
properties.addNewVMerge().setVal(i == 0 ? STMerge.RESTART : STMerge.CONTINUE);
|
if (!properties.isSetTextDirection()) {
|
properties.addNewTextDirection();
|
}
|
properties.getTextDirection().setVal(STTextDirection.BT_LR);
|
if (!properties.isSetVAlign()) {
|
properties.addNewVAlign();
|
}
|
properties.getVAlign().setVal(STVerticalJc.CENTER);
|
}
|
}
|
|
private void configureRecordRows(CTTbl table, int headerRowCount) {
|
clearTablePosition(table);
|
for (int i = 0; i < table.sizeOfTrArray(); i++) {
|
CTRow row = table.getTrArray(i);
|
CTTrPr properties = row.isSetTrPr() ? row.getTrPr() : row.addNewTrPr();
|
if (properties.sizeOfCantSplitArray() == 0) {
|
properties.addNewCantSplit();
|
}
|
while (properties.sizeOfTblHeaderArray() > 0) {
|
properties.removeTblHeader(0);
|
}
|
if (i < headerRowCount) {
|
properties.addNewTblHeader();
|
}
|
configureCellParagraphPagination(row, false);
|
}
|
}
|
|
private int tableWidth(CTTbl table) {
|
CTTblGrid grid = table.getTblGrid();
|
if (grid != null) {
|
int width = 0;
|
for (int i = 0; i < grid.sizeOfGridColArray(); i++) {
|
Object value = grid.getGridColArray(i).getW();
|
if (value != null) {
|
width += new BigInteger(String.valueOf(value)).intValue();
|
}
|
}
|
if (width > 0) {
|
return width;
|
}
|
}
|
return totalWidth(TABLE_COLUMN_WIDTHS);
|
}
|
|
private void noBorder(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder border) {
|
border.setVal(STBorder.NIL);
|
border.setSz(BigInteger.ZERO);
|
border.setSpace(BigInteger.ZERO);
|
}
|
|
private void zeroMargin(CTTblWidth margin) {
|
margin.setType(STTblWidth.DXA);
|
margin.setW(BigInteger.ZERO);
|
}
|
|
private void configureCellParagraphPagination(CTRow row, boolean keepWithNext) {
|
// Keep the complete row together. Word measures the final rendered text height dynamically,
|
// so no fixed row height or per-page record count is needed.
|
for (CTTc cell : row.getTcList()) {
|
for (CTP paragraph : cell.getPList()) {
|
CTPPr properties = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr();
|
if (!properties.isSetKeepLines()) {
|
properties.addNewKeepLines();
|
}
|
if (properties.isSetKeepNext()) {
|
properties.unsetKeepNext();
|
}
|
if (keepWithNext) {
|
properties.addNewKeepNext();
|
}
|
}
|
}
|
}
|
|
private void copyTemplatePrefixToHeader(XWPFDocument document, List<XmlObject> elements,
|
int detailTableIndex, Map<String, String> values,
|
List<Map<String, String>> records) {
|
XWPFHeader header = document.createHeader(HeaderFooterType.DEFAULT);
|
CTHdrFtr headerContent = header._getHdrFtr();
|
for (int i = 0; i < detailTableIndex; i++) {
|
XmlObject element = elements.get(i);
|
if (element instanceof CTP) {
|
CTP paragraph = headerContent.addNewP();
|
paragraph.set(element);
|
replaceInParagraph(paragraph, values);
|
} else if (element instanceof CTTbl) {
|
appendHeaderFooterTable(headerContent, (CTTbl) element, values, records);
|
}
|
}
|
}
|
|
private void copyTemplateTableToFooter(XWPFDocument document, CTTbl source,
|
Map<String, String> values, List<Map<String, String>> records) {
|
XWPFFooter footer = document.createFooter(HeaderFooterType.DEFAULT);
|
appendHeaderFooterTable(footer._getHdrFtr(), source, values, records);
|
}
|
|
private void appendHeaderFooterTable(CTHdrFtr content, CTTbl source,
|
Map<String, String> values,
|
List<Map<String, String>> records) {
|
CTTbl table = content.addNewTbl();
|
table.set(source);
|
clearTablePosition(table);
|
renderTable(table, values, records);
|
}
|
|
private void clearTablePosition(CTTbl table) {
|
CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
|
if (properties.isSetTblpPr()) {
|
properties.unsetTblpPr();
|
}
|
}
|
|
private void renderTable(CTTbl table, Map<String, String> unitValues,
|
List<Map<String, String>> records) {
|
boolean expand = containsDetailPlaceholder(table);
|
int rowCount = table.sizeOfTrArray();
|
if (rowCount == 0) {
|
return;
|
}
|
if (expand) {
|
CTRow dataRow = table.getTrArray(rowCount - 1);
|
for (CTTc cell : dataRow.getTcList()) {
|
CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
|
if (properties.isSetVMerge()) {
|
properties.unsetVMerge();
|
}
|
}
|
}
|
|
int dataRowIndex = rowCount - 1;
|
if (expand) {
|
CTRow dataRow = table.getTrArray(dataRowIndex);
|
for (int i = 0; i < records.size(); i++) {
|
table.addNewTr().set(dataRow);
|
}
|
table.removeTr(dataRowIndex);
|
}
|
for (int i = 0; i < table.sizeOfTrArray(); i++) {
|
boolean dataRow = expand && i >= table.sizeOfTrArray() - records.size();
|
Map<String, String> values = dataRow
|
? records.get(i - (table.sizeOfTrArray() - records.size()))
|
: unitValues;
|
replaceInRow(table.getTrArray(i), values);
|
}
|
}
|
|
private boolean containsDetailPlaceholder(CTTbl table) {
|
StringBuilder builder = new StringBuilder();
|
for (CTRow row : table.getTrList()) {
|
for (CTTc cell : row.getTcList()) {
|
for (CTP paragraph : cell.getPList()) {
|
for (CTR run : paragraph.getRList()) {
|
for (CTText text : run.getTList()) {
|
builder.append(text.getStringValue());
|
}
|
}
|
}
|
}
|
}
|
|
Matcher matcher = TEMPLATE_PLACEHOLDER.matcher(builder.toString());
|
while (matcher.find()) {
|
String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
|
if (DETAIL_PLACEHOLDERS.contains(key.trim())) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
private void replaceInRow(CTRow row, Map<String, String> values) {
|
for (CTTc cell : row.getTcList()) {
|
for (CTP paragraph : cell.getPList()) {
|
replaceInParagraph(paragraph, values);
|
}
|
}
|
}
|
|
private void replaceInParagraph(CTP paragraph, Map<String, String> values) {
|
List<CTR> runs = paragraph.getRList();
|
if (runs.isEmpty()) {
|
return;
|
}
|
|
List<TextChunk> chunks = new ArrayList<>();
|
StringBuilder builder = new StringBuilder();
|
for (CTR run : runs) {
|
for (CTText text : run.getTList()) {
|
String value = text.getStringValue();
|
if (value == null) {
|
value = "";
|
}
|
chunks.add(new TextChunk(text, builder.length(), builder.length() + value.length()));
|
builder.append(value);
|
}
|
}
|
|
String original = builder.toString();
|
if (original.indexOf('{') < 0) {
|
return;
|
}
|
|
Map<CTText, List<TextEdit>> edits = new LinkedHashMap<>();
|
Matcher matcher = TEMPLATE_PLACEHOLDER.matcher(original);
|
while (matcher.find()) {
|
String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
|
String replacement = values.get(key.trim());
|
if (replacement == null) {
|
replacement = "";
|
}
|
for (TextChunk chunk : chunks) {
|
int overlapStart = Math.max(matcher.start(), chunk.start);
|
int overlapEnd = Math.min(matcher.end(), chunk.end);
|
if (overlapStart >= overlapEnd) {
|
continue;
|
}
|
String inserted = overlapStart == matcher.start() ? replacement : "";
|
edits.computeIfAbsent(chunk.text, item -> new ArrayList<>())
|
.add(new TextEdit(overlapStart - chunk.start, overlapEnd - chunk.start, inserted));
|
}
|
}
|
|
for (Map.Entry<CTText, List<TextEdit>> entry : edits.entrySet()) {
|
String value = text(entry.getKey().getStringValue());
|
entry.getValue().sort((left, right) -> Integer.compare(right.start, left.start));
|
for (TextEdit edit : entry.getValue()) {
|
value = value.substring(0, edit.start) + edit.replacement + value.substring(edit.end);
|
}
|
entry.getKey().setStringValue(value);
|
}
|
}
|
|
private static final class TextChunk {
|
private final CTText text;
|
private final int start;
|
private final int end;
|
|
private TextChunk(CTText text, int start, int end) {
|
this.text = text;
|
this.start = start;
|
this.end = end;
|
}
|
}
|
|
private static final class TextEdit {
|
private final int start;
|
private final int end;
|
private final String replacement;
|
|
private TextEdit(int start, int end, String replacement) {
|
this.start = start;
|
this.end = end;
|
this.replacement = replacement;
|
}
|
}
|
|
private void replaceInParagraphLegacy(CTP paragraph, Map<String, String> values) {
|
List<CTR> runs = paragraph.getRList();
|
if (runs.isEmpty()) {
|
return;
|
}
|
StringBuilder builder = new StringBuilder();
|
for (CTR run : runs) {
|
for (CTText text : run.getTList()) {
|
builder.append(text.getStringValue());
|
}
|
}
|
String original = builder.toString();
|
if (original.indexOf('{') < 0) {
|
return;
|
}
|
String replaced = substitute(original, values);
|
if (replaced.equals(original)) {
|
return;
|
}
|
if (runs.size() == 1) {
|
CTR run = runs.get(0);
|
if (run.sizeOfTArray() > 0) {
|
run.getTArray(0).setStringValue(replaced);
|
for (int i = run.sizeOfTArray() - 1; i >= 1; i--) {
|
run.removeT(i);
|
}
|
return;
|
}
|
}
|
for (CTR run : runs) {
|
for (int i = run.sizeOfTArray() - 1; i >= 0; i--) {
|
run.removeT(i);
|
}
|
for (int i = run.sizeOfTabArray() - 1; i >= 0; i--) {
|
run.removeTab(i);
|
}
|
for (int i = run.sizeOfBrArray() - 1; i >= 0; i--) {
|
run.removeBr(i);
|
}
|
}
|
runs.get(0).addNewT().setStringValue(replaced);
|
}
|
|
private String substitute(String text, Map<String, String> values) {
|
Matcher matcher = PLACEHOLDER.matcher(text);
|
StringBuffer buffer = new StringBuffer();
|
while (matcher.find()) {
|
String key = matcher.group(1).trim();
|
String value = values.get(key);
|
matcher.appendReplacement(buffer, Matcher.quoteReplacement(value == null ? "" : value));
|
}
|
matcher.appendTail(buffer);
|
return buffer.toString();
|
}
|
|
private void appendPageBreakParagraph(CTBody body) {
|
CTP paragraph = body.addNewP();
|
CTPPr properties = paragraph.addNewPPr();
|
properties.addNewPageBreakBefore();
|
properties.addNewSpacing().setBefore(BigInteger.ZERO);
|
properties.getSpacing().setAfter(BigInteger.ZERO);
|
properties.addNewRPr().addNewSz().setVal(BigInteger.valueOf(2));
|
}
|
|
private void setPageBreakBefore(CTP paragraph) {
|
CTPPr properties = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr();
|
if (!properties.isSetPageBreakBefore()) {
|
properties.addNewPageBreakBefore();
|
}
|
}
|
|
private void moveSectionPropertiesToEnd(CTBody body) {
|
if (!body.isSetSectPr()) {
|
return;
|
}
|
CTSectPr section = (CTSectPr) body.getSectPr().copy();
|
body.unsetSectPr();
|
body.setSectPr(section);
|
}
|
|
private String collectText(XWPFDocument document) {
|
StringBuilder builder = new StringBuilder();
|
for (XWPFParagraph paragraph : document.getParagraphs()) {
|
builder.append(paragraph.getText()).append('\n');
|
}
|
for (XWPFTable table : document.getTables()) {
|
for (XWPFTableRow row : table.getRows()) {
|
for (XWPFTableCell cell : row.getTableCells()) {
|
builder.append(cell.getText()).append('\n');
|
}
|
}
|
}
|
return builder.toString();
|
}
|
|
private InputStream openTemplate() throws IOException {
|
Path path = templatePath();
|
if (path != null && Files.exists(path)) {
|
return Files.newInputStream(path);
|
}
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
try (XWPFDocument document = buildDefaultTemplate()) {
|
document.write(buffer);
|
}
|
return new ByteArrayInputStream(buffer.toByteArray());
|
}
|
|
private Path templatePath() {
|
if (!StringUtils.hasText(templateLocation)) {
|
return null;
|
}
|
return Paths.get(templateLocation).toAbsolutePath().normalize();
|
}
|
|
|
/** 生成一份带占位符的默认工程单模板。 */
|
XWPFDocument buildDefaultTemplate() throws IOException {
|
XWPFDocument document = new XWPFDocument();
|
CTBody body = document.getDocument().getBody();
|
CTSectPr section = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
|
CTPageSz pageSize = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
|
pageSize.setW(A4_LANDSCAPE_WIDTH);
|
pageSize.setH(A4_LANDSCAPE_HEIGHT);
|
pageSize.setOrient(STPageOrientation.LANDSCAPE);
|
CTPageMar margin = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
|
margin.setTop(PAGE_MARGIN);
|
margin.setBottom(PAGE_MARGIN);
|
margin.setLeft(PAGE_MARGIN);
|
margin.setRight(PAGE_MARGIN);
|
int mainColumns = DEFAULT_COLUMNS.length;
|
int columns = mainColumns + 1;
|
XWPFTable table = document.createTable(3, columns);
|
applyBorders(table.getCTTbl());
|
applyGrid(table.getCTTbl(), TABLE_COLUMN_WIDTHS);
|
|
XWPFTableRow headerRow = table.getRow(0);
|
mergeMainColumns(headerRow, mainColumns, document);
|
setCellText(headerRow.getCell(0), DEFAULT_UNIT_HEADER, true);
|
|
XWPFTableRow titleRow = table.getRow(1);
|
XWPFTableRow fieldRow = table.getRow(2);
|
for (int i = 0; i < mainColumns; i++) {
|
setWidth(titleRow.getCell(i), TABLE_COLUMN_WIDTHS[i]);
|
setWidth(fieldRow.getCell(i), TABLE_COLUMN_WIDTHS[i]);
|
setCellText(titleRow.getCell(i), DEFAULT_COLUMNS[i][0], true);
|
setCellText(fieldRow.getCell(i), DEFAULT_COLUMNS[i][1], false);
|
}
|
configureUnitColumn(titleRow, mainColumns, false);
|
configureUnitColumn(fieldRow, mainColumns, false);
|
|
moveSectionPropertiesToEnd(body);
|
document.getProperties().getCoreProperties().setKeywords(TEMPLATE_VERSION);
|
return document;
|
}
|
|
private void applyBorders(CTTbl table) {
|
CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
|
CTTblBorders borders = properties.isSetTblBorders() ? properties.getTblBorders() : properties.addNewTblBorders();
|
border(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
|
border(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
|
border(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
|
border(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
|
border(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
|
border(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
|
}
|
|
private void border(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder border) {
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(4));
|
border.setColor("000000");
|
border.setSpace(BigInteger.ZERO);
|
}
|
|
private void applyGrid(CTTbl table, int[] widths) {
|
CTTblGrid grid = table.getTblGrid() != null ? table.getTblGrid() : table.addNewTblGrid();
|
while (grid.sizeOfGridColArray() > widths.length) {
|
grid.removeGridCol(grid.sizeOfGridColArray() - 1);
|
}
|
while (grid.sizeOfGridColArray() < widths.length) {
|
grid.addNewGridCol();
|
}
|
for (int i = 0; i < widths.length; i++) {
|
grid.getGridColArray(i).setW(BigInteger.valueOf(widths[i]));
|
}
|
CTTblPr properties = table.getTblPr() != null ? table.getTblPr() : table.addNewTblPr();
|
CTTblWidth width = properties.isSetTblW() ? properties.getTblW() : properties.addNewTblW();
|
width.setType(STTblWidth.DXA);
|
width.setW(BigInteger.valueOf(totalWidth(widths)));
|
}
|
|
private int totalWidth(int[] widths) {
|
int total = 0;
|
for (int value : widths) {
|
total += value;
|
}
|
return total;
|
}
|
|
private void mergeMainColumns(XWPFTableRow row, int mainColumns, XWPFDocument document) {
|
CTRow ctRow = row.getCtRow();
|
while (ctRow.sizeOfTcArray() > 2) {
|
ctRow.removeTc(ctRow.sizeOfTcArray() - 2);
|
}
|
CTTc cell = ctRow.getTcArray(0);
|
CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
|
if (!properties.isSetGridSpan()) {
|
properties.addNewGridSpan().setVal(BigInteger.valueOf(mainColumns));
|
}
|
CTTblWidth cellWidth = properties.isSetTcW() ? properties.getTcW() : properties.addNewTcW();
|
cellWidth.setType(STTblWidth.DXA);
|
cellWidth.setW(BigInteger.valueOf(totalWidth(TABLE_COLUMN_WIDTHS) - UNIT_COLUMN_WIDTH));
|
|
CTTc unitCell = ctRow.getTcArray(1);
|
XWPFTableCell unitWrapper = new XWPFTableCell(unitCell, row, document);
|
setWidth(unitWrapper, UNIT_COLUMN_WIDTH);
|
configureUnitCell(unitCell, true);
|
setCellText(unitWrapper, "{unitDisplay}", true);
|
}
|
|
private void configureUnitColumn(XWPFTableRow row, int mainColumns, boolean restart) {
|
int index = mainColumns;
|
XWPFTableCell cell = row.getCell(index);
|
setWidth(cell, UNIT_COLUMN_WIDTH);
|
configureUnitCell(cell.getCTTc(), restart);
|
}
|
|
private void configureUnitCell(CTTc cell, boolean restart) {
|
CTTcPr properties = cell.isSetTcPr() ? cell.getTcPr() : cell.addNewTcPr();
|
if (!properties.isSetVMerge()) {
|
properties.addNewVMerge();
|
}
|
properties.getVMerge().setVal(restart ? STMerge.RESTART : STMerge.CONTINUE);
|
if (!properties.isSetTextDirection()) {
|
properties.addNewTextDirection();
|
}
|
properties.getTextDirection().setVal(STTextDirection.BT_LR);
|
if (!properties.isSetVAlign()) {
|
properties.addNewVAlign();
|
}
|
properties.getVAlign().setVal(STVerticalJc.CENTER);
|
}
|
|
private void setWidth(XWPFTableCell cell, int width) {
|
CTTcPr properties = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
|
CTTblWidth cellWidth = properties.isSetTcW() ? properties.getTcW() : properties.addNewTcW();
|
cellWidth.setType(STTblWidth.DXA);
|
cellWidth.setW(BigInteger.valueOf(width));
|
}
|
|
private void setCellText(XWPFTableCell cell, String text, boolean centered) {
|
XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
|
if (centered) {
|
paragraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.CENTER);
|
}
|
org.apache.poi.xwpf.usermodel.XWPFRun run = paragraph.createRun();
|
run.setFontFamily("SimSun");
|
run.setFontSize(9);
|
run.setText(text);
|
for (XWPFParagraph extra : new ArrayList<>(cell.getParagraphs())) {
|
if (extra != paragraph) {
|
cell.removeParagraph(cell.getParagraphs().indexOf(extra));
|
}
|
}
|
}
|
|
|
private static String text(String value) {
|
return value == null ? "" : value;
|
}
|
|
private static String date(LocalDate value) {
|
return value == null ? "" : DATE_FORMAT.format(value);
|
}
|
|
private static String yesNo(Integer value) {
|
return value != null && value == 1 ? "是" : "否";
|
}
|
|
private static String firstText(String... candidates) {
|
for (String candidate : candidates) {
|
if (StringUtils.hasText(candidate)) {
|
return candidate;
|
}
|
}
|
return "";
|
}
|
}
|