zhizhijie
2026-10-06 bee9e18c3af284ff432a4472f1d409f5a6dc09a7
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
New file
@@ -0,0 +1,554 @@
package com.trafficaudit.holiday.service;
import com.trafficaudit.holiday.HolidayNumberUtils;
import com.trafficaudit.holiday.entity.HolidayCalendar;
import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.annotation.Resource;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@Slf4j
@Service
public class HolidayBulletinService {
    private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
    private static final BigDecimal EPSILON = new BigDecimal("0.0000001");
    private static final int EXCEL_ONE_DECIMAL_PERCENT = 1;
    private static final int EXCEL_TWO_DECIMAL_PERCENT = 2;
    private static final int EXCEL_SEVEN_DECIMAL_YI = 7;
    private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000");
    @Value("${holiday.bulletin-template:docs/专项统计/重要节假日/湖北省国庆假期综合运输客流快报模板.docx}")
    private String bulletinTemplatePath;
    private final HolidayStatService statService;
    @Resource
    private HolidayConfigService configService;
    private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>();
    public HolidayBulletinService(HolidayStatService statService) {
        this.statService = statService;
    }
    public byte[] generate(LocalDate date) {
        DailyMetrics current = statService.findMetricsByDate(date);
        if (current == null) {
            throw new IllegalArgumentException("所选日期不在已配置的节假日范围内");
        }
        DailyMetrics previousDay = current.getDayIndex() > 1
                ? statService.ringRatioBaseMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()) : null;
        HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType());
        LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null
                ? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate();
        DailyMetrics previousCalendarDay = previousDay != null
                ? previousDay : statService.dailyMetricsByDate(previousDayDate);
        DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
        DailyMetrics twoYearsAgo = statService.dailyMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex());
        DailyMetrics currentCumulative = statService.cumulativeMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex());
        DailyMetrics previousYearCumulative = statService.cumulativeMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
        DailyMetrics twoYearsAgoCumulative = statService.cumulativeMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex());
        if (current.getTotalPersonFlow() == null) {
            throw new IllegalArgumentException("所选日期的全社会跨区域人员流动量没有足够数据,无法生成快报");
        }
        String currentTotal = formatNumber(current.getTotalPersonFlow());
        String ringRatioClause = previousDay == null ? "" : ",环比" + ratioPhrase(
                current.getTotalPersonFlow(), previousDay.getTotalPersonFlow(), EXCEL_TWO_DECIMAL_PERCENT);
        String paragraph1 = String.format("%d月%d日(假期第%d天),全省全社会跨区域人员流动量%s万人次%s,比2025年同期%s,比2024年同期%s。",
                date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal,
                ringRatioClause,
                ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT),
                ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT));
        String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
        File template = resolveTemplateFile();
        if (!template.exists() || !template.isFile()) {
            throw new IllegalArgumentException("快报模板文件不存在: " + template.getAbsolutePath());
        }
        try {
            byte[] templateBytes;
            try (InputStream in = new FileInputStream(template)) {
                templateBytes = readAll(in);
            }
            byte[] documentXml = readZipEntry(templateBytes, "word/document.xml");
            Document document = parseDocument(documentXml);
            CURRENT_DOCUMENT.set(document);
            try {
                replaceParagraph(document, text -> text.startsWith("10月") && text.contains("假期第"), paragraph1);
                replaceParagraph(document, text -> text.startsWith("假期以来"), paragraph2);
                fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo,
                        currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
                byte[] replacedXml = serializeDocument(document);
                return replaceZipEntry(templateBytes, "word/document.xml", replacedXml);
            } finally {
                CURRENT_DOCUMENT.remove();
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("快报生成失败: " + e.getMessage(), e);
        }
    }
    private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay,
                           DailyMetrics previousCalendarDay, DailyMetrics previousYear, DailyMetrics twoYearsAgo,
                           DailyMetrics currentCumulative, DailyMetrics previousYearCumulative,
                           DailyMetrics twoYearsAgoCumulative) {
        DailyMetrics firstDayRailBase = previousDay == null ? previousCalendarDay : previousDay;
        DailyMetrics firstDayCivilAviationBase = previousDay == null ? previousCalendarDay : previousDay;
        Element table = firstElement(document, "tbl");
        if (table == null) {
            throw new IllegalArgumentException("快报模板中找不到数据表");
        }
        DailyMetrics previousYearCivilCumulative = comparableCumulative(
                current, previousYear, DailyMetrics::getCivilAviation);
        DailyMetrics twoYearsAgoCivilCumulative = comparableCumulative(
                current, twoYearsAgo, DailyMetrics::getCivilAviation);
        writeTableRow(table, 2, new String[]{
                number(current.getNonBusinessPersonFlow()),
                ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                numberCumulative(currentCumulative.getNonBusinessPersonFlow()),
                ratio(currentCumulative.getNonBusinessPersonFlow(),
                        previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(currentCumulative.getNonBusinessPersonFlow(),
                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT)
        });
        writeTableRow(table, 3, new String[]{
                number(current.getRoad()),
                ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_TWO_DECIMAL_PERCENT),
                number(currentCumulative.getRoad()),
                ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad(),
                        EXCEL_ONE_DECIMAL_PERCENT)
        });
        writeTableRow(table, 4, new String[]{
                number(current.getRoadPersonFlow()),
                ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                numberCumulative(currentCumulative.getRoadPersonFlow()),
                ratio(currentCumulative.getRoadPersonFlow(),
                        previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(currentCumulative.getRoadPersonFlow(),
                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT)
        });
        writeTableRow(table, 5, transportRow(current.getRail(),
                firstDayRailBase == null ? null : firstDayRailBase.getRail(),
                previousYear == null ? null : previousYear.getRail(),
                twoYearsAgo == null ? null : twoYearsAgo.getRail(),
                currentCumulative.getRail(),
                previousYearCumulative == null ? null : previousYearCumulative.getRail(),
                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail(),
                EXCEL_ONE_DECIMAL_PERCENT));
        writeTableRow(table, 6, transportRow(current.getCivilAviation(),
                firstDayCivilAviationBase == null ? null : firstDayCivilAviationBase.getCivilAviation(),
                previousYear == null ? null : previousYear.getCivilAviation(),
                twoYearsAgo == null ? null : twoYearsAgo.getCivilAviation(),
                currentCumulative.getCivilAviation(),
                previousYearCivilCumulative == null ? null : previousYearCivilCumulative.getCivilAviation(),
                twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation(),
                EXCEL_ONE_DECIMAL_PERCENT));
        writeTableRow(table, 7, transportRow(current.getWater(),
                previousDay == null ? null : previousDay.getWater(),
                previousYear == null ? null : previousYear.getWater(),
                twoYearsAgo == null ? null : twoYearsAgo.getWater(),
                currentCumulative.getWater(),
                previousYearCumulative == null ? null : previousYearCumulative.getWater(),
                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater(),
                EXCEL_TWO_DECIMAL_PERCENT));
        writeTableRow(table, 8, new String[]{
                number(current.getTotalPersonFlow()),
                ratio(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(),
                        EXCEL_ONE_DECIMAL_PERCENT),
                numberCumulative(currentCumulative.getTotalPersonFlow()),
                ratio(currentCumulative.getTotalPersonFlow(),
                        previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratio(currentCumulative.getTotalPersonFlow(),
                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT)
        });
    }
    private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear,
                                  BigDecimal twoYearsAgo, BigDecimal currentCumulative,
                                  BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative,
                                  int ringRatioExcelScale) {
        return new String[]{
                number(current),
                ratio(current, previousDay, ringRatioExcelScale),
                ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT),
                ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT),
                number(currentCumulative),
                ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT),
                ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT)
        };
    }
    private void writeTableRow(Element table, int rowIndex, String[] values) {
        List<Element> rows = childElements(table, "tr");
        if (rowIndex >= rows.size()) return;
        Element row = rows.get(rowIndex);
        List<Element> cells = cellRefs(row);
        int logicalColumn = 0;
        for (Element cell : cells) {
            int span = gridSpan(cell);
            if (logicalColumn >= 2) {
                int valueIndex = logicalColumn - 2;
                if (valueIndex < values.length) {
                    setCellText(cell, values[valueIndex]);
                }
            }
            logicalColumn += span;
        }
    }
    private List<Element> cellRefs(Element row) {
        List<Element> cells = new ArrayList<>();
        for (Element child : childElements(row, "tc")) cells.add(child);
        return cells;
    }
    private int gridSpan(Element cell) {
        Element tcPr = firstChildElement(cell, "tcPr");
        if (tcPr == null) return 1;
        Element span = firstChildElement(tcPr, "gridSpan");
        if (span == null) return 1;
        String value = span.getAttributeNS(WORD_NS, "val");
        try {
            return Integer.parseInt(value);
        } catch (Exception e) {
            return 1;
        }
    }
    private void setCellText(Element cell, String text) {
        Element tcPr = firstChildElement(cell, "tcPr");
        Element templateParagraph = firstChildElement(cell, "p");
        Element paragraph = createElement("p");
        if (templateParagraph != null) {
            Element pPr = firstChildElement(templateParagraph, "pPr");
            if (pPr != null) paragraph.appendChild(pPr.cloneNode(true));
        }
        Element templateRun = templateParagraph == null ? null : firstChildElement(templateParagraph, "r");
        Element rPr = templateRun == null ? null : firstChildElement(templateRun, "rPr");
        Element run = createElement("r");
        if (rPr != null) run.appendChild(rPr.cloneNode(true));
        Element t = createElement("t");
        t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
        t.setTextContent(text == null ? "" : text);
        run.appendChild(t);
        paragraph.appendChild(run);
        List<Node> removals = new ArrayList<>();
        Node child = cell.getFirstChild();
        while (child != null) {
            Node next = child.getNextSibling();
            if (child instanceof Element && "p".equals(localName(child))) {
                removals.add(child);
            }
            child = next;
        }
        for (Node node : removals) cell.removeChild(node);
        if (tcPr != null && tcPr.getNextSibling() != null) {
            cell.insertBefore(paragraph, tcPr.getNextSibling());
        } else if (tcPr != null) {
            cell.appendChild(paragraph);
        } else {
            cell.insertBefore(paragraph, cell.getFirstChild());
        }
    }
    private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) {
        NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p");
        for (int i = 0; i < paragraphs.getLength(); i++) {
            Element paragraph = (Element) paragraphs.item(i);
            String text = paragraphText(paragraph);
            if (matcher.matches(text)) {
                setParagraphText(paragraph, replacement);
                return;
            }
        }
        throw new IllegalArgumentException("快报模板缺少可替换段落: " + replacement);
    }
    private void setParagraphText(Element paragraph, String text) {
        Element pPr = firstChildElement(paragraph, "pPr");
        Element firstRun = firstChildElement(paragraph, "r");
        Element rPr = firstRun == null ? null : firstChildElement(firstRun, "rPr");
        List<Node> removals = new ArrayList<>();
        Node child = paragraph.getFirstChild();
        while (child != null) {
            Node next = child.getNextSibling();
            if (child != pPr) removals.add(child);
            child = next;
        }
        for (Node node : removals) paragraph.removeChild(node);
        Element run = createElement("r");
        if (rPr != null) run.appendChild(rPr.cloneNode(true));
        Element t = createElement("t");
        t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
        t.setTextContent(text);
        run.appendChild(t);
        paragraph.appendChild(run);
    }
    private String paragraphText(Element paragraph) {
        StringBuilder builder = new StringBuilder();
        NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t");
        for (int i = 0; i < texts.getLength(); i++) {
            builder.append(texts.item(i).getTextContent());
        }
        return builder.toString();
    }
    private String ratioPhrase(BigDecimal current, BigDecimal base, int excelPercentScale) {
        BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
        if (percent == null) return "无可比数据";
        if (percent.signum() == 0) return "持平";
        return (percent.signum() > 0 ? "增长" : "下降") + HolidayNumberUtils.format1(percent.abs()) + "%";
    }
    private DailyMetrics comparableCumulative(DailyMetrics current, DailyMetrics comparisonYear,
                                              Function<DailyMetrics, BigDecimal> selector) {
        if (current == null || comparisonYear == null) return null;
        int coveredDay = 0;
        for (int day = current.getDayIndex(); day >= 1; day--) {
            DailyMetrics metrics = statService.dailyMetrics(current.getYear(), current.getHolidayType(), day);
            if (metrics != null && selector.apply(metrics) != null) {
                coveredDay = day;
                break;
            }
        }
        if (coveredDay == 0) return null;
        return statService.cumulativeMetrics(comparisonYear.getYear(), current.getHolidayType(), coveredDay);
    }
    private String cumulativeParagraph(DailyMetrics currentCumulative,
                                       DailyMetrics previousYearCumulative,
                                       DailyMetrics twoYearsAgoCumulative) {
        BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow();
        return String.format("假期以来,全省全社会跨区域人员流动量累计%s万人次,比2025年同期%s,比2024年同期%s。",
                formatNumber(displayedCumulativeWan(total)),
                ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT),
                ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
                        EXCEL_TWO_DECIMAL_PERCENT));
    }
    private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) {
        BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
        if (percent == null) return "—";
        return percent.signum() == 0 ? "0%" : percent.toPlainString() + "%";
    }
    /**
     * Mirrors the source Excel percentage cell format, then applies the bulletin's one-decimal rule.
     * Excel stores full precision but displays 0.0% or 0.00%; reviewers compare against that display.
     */
    private BigDecimal bulletinPercent(BigDecimal current, BigDecimal base, int excelPercentScale) {
        if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return null;
        BigDecimal ratio = HolidayNumberUtils.divide(current, base);
        if (ratio == null) return null;
        BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
        if (percent == null) return null;
        BigDecimal displayed = percent.setScale(excelPercentScale, RoundingMode.HALF_UP);
        return displayed.setScale(1, RoundingMode.HALF_UP);
    }
    private String number(BigDecimal value) {
        return value == null ? "—" : formatNumber(value);
    }
    private String numberCumulative(BigDecimal value) {
        return value == null ? "—" : formatNumber(displayedCumulativeWan(value));
    }
    /**
     * Source cumulative cells U5/X5/AA5 are stored in 100-million units and displayed with seven decimals.
     * Mirror that displayed precision before the bulletin converts the value to 10-thousand units.
     */
    private BigDecimal displayedCumulativeWan(BigDecimal cumulativeWan) {
        if (cumulativeWan == null) return null;
        BigDecimal yi = cumulativeWan.divide(TEN_THOUSAND, EXCEL_SEVEN_DECIMAL_YI, RoundingMode.HALF_UP);
        return HolidayNumberUtils.normalize(yi.multiply(TEN_THOUSAND));
    }
    private String formatNumber(BigDecimal value) {
        if (value == null) return "";
        return HolidayNumberUtils.format2(value);
    }
    private File resolveTemplateFile() {
        List<File> candidates = new ArrayList<>();
        File configured = new File(bulletinTemplatePath);
        candidates.add(configured);
        if (!configured.isAbsolute()) {
            candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath));
            candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath));
        }
        for (File candidate : candidates) {
            try {
                if (candidate.exists() && candidate.isFile()) {
                    return candidate.getCanonicalFile();
                }
            } catch (Exception ignored) {
                // Keep checking.
            }
        }
        return configured;
    }
    private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception {
        try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
            ZipEntry entry;
            while ((entry = zin.getNextEntry()) != null) {
                if (entryName.equals(entry.getName())) {
                    return readAll(zin);
                }
            }
        }
        throw new IllegalArgumentException("模板中缺少 " + entryName);
    }
    private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes));
             ZipOutputStream zout = new ZipOutputStream(out)) {
            ZipEntry entry;
            byte[] buffer = new byte[8192];
            while ((entry = zin.getNextEntry()) != null) {
                ZipEntry target = new ZipEntry(entry.getName());
                zout.putNextEntry(target);
                if (entryName.equals(entry.getName())) {
                    zout.write(replacement);
                } else {
                    int length;
                    while ((length = zin.read(buffer)) > 0) {
                        zout.write(buffer, 0, length);
                    }
                }
                zout.closeEntry();
            }
        }
        return out.toByteArray();
    }
    private Document parseDocument(byte[] xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
    }
    private byte[] serializeDocument(Document document) throws Exception {
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        transformer.transform(new DOMSource(document), new StreamResult(out));
        return out.toByteArray();
    }
    private Element firstElement(Document document, String localName) {
        NodeList list = document.getElementsByTagNameNS(WORD_NS, localName);
        return list.getLength() == 0 ? null : (Element) list.item(0);
    }
    private Element createElement(String localName) {
        Document document = CURRENT_DOCUMENT.get();
        if (document == null) {
            throw new IllegalStateException("Word document is not initialized");
        }
        return document.createElementNS(WORD_NS, "w:" + localName);
    }
    private List<Element> childElements(Element parent, String localName) {
        List<Element> result = new ArrayList<>();
        Node child = parent.getFirstChild();
        while (child != null) {
            if (child instanceof Element && localName.equals(localName(child))) {
                result.add((Element) child);
            }
            child = child.getNextSibling();
        }
        return result;
    }
    private Element firstChildElement(Element parent, String localName) {
        Node child = parent.getFirstChild();
        while (child != null) {
            if (child instanceof Element && localName.equals(localName(child))) {
                return (Element) child;
            }
            child = child.getNextSibling();
        }
        return null;
    }
    private String localName(Node node) {
        return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName();
    }
    private byte[] readAll(InputStream in) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[8192];
        int length;
        while ((length = in.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }
        return out.toByteArray();
    }
    private interface ParagraphMatcher {
        boolean matches(String text);
    }
}