package jnpf.dmsService; import feign.Response; import jnpf.file.FileUploadApi; import jnpf.model.upload.UploadFileModel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** Reads the flow-version binding from a qianpiye DOCX without exposing file bytes to the plugin. */ @Service @RequiredArgsConstructor @Slf4j public class DmsQianpiyeDocxPropertiesReader { private static final String CUSTOM_PROPERTIES_PART = "docProps/custom.xml"; private static final String NEW_FLOW_VERSION_PROPERTY = "DMS_NEW_FLOW_VERSION_ID"; private static final String EDIT_FLOW_VERSION_PROPERTY = "DMS_EDIT_FLOW_VERSION_ID"; private static final int MAX_CUSTOM_PROPERTIES_BYTES = 512 * 1024; private final FileUploadApi fileUploadApi; public Map read(String fileType, String fileId) { if (!StringUtils.hasText(fileType) || !StringUtils.hasText(fileId)) return Map.of(); try (Response response = fileUploadApi.downFile(new UploadFileModel(fileType, fileId))) { if (response == null || response.status() < 200 || response.status() >= 300 || response.body() == null) { return Map.of(); } try (InputStream input = response.body().asInputStream()) { byte[] custom = findCustomProperties(input); return custom == null ? Map.of() : parse(custom); } } catch (Exception e) { // Existing templates must remain editable when their original file is unavailable. log.debug("读取签批页 DOCX 自定义属性失败,file={}", fileId, e); return Map.of(); } } public static boolean hasCompleteFlowVersions(Map properties) { return StringUtils.hasText(properties.get(NEW_FLOW_VERSION_PROPERTY)) && StringUtils.hasText(properties.get(EDIT_FLOW_VERSION_PROPERTY)); } public static String newFlowVersionId(Map properties) { return properties.get(NEW_FLOW_VERSION_PROPERTY); } public static String revisionFlowVersionId(Map properties) { return properties.get(EDIT_FLOW_VERSION_PROPERTY); } private byte[] findCustomProperties(InputStream source) throws Exception { try (ZipInputStream input = new ZipInputStream(source)) { ZipEntry entry; while ((entry = input.getNextEntry()) != null) { if (!CUSTOM_PROPERTIES_PART.equals(entry.getName())) continue; ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int total = 0; for (int read; (read = input.read(buffer)) >= 0; ) { total += read; if (total > MAX_CUSTOM_PROPERTIES_BYTES) return null; output.write(buffer, 0, read); } return output.toByteArray(); } } return null; } private Map parse(byte[] source) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); org.w3c.dom.Document document = factory.newDocumentBuilder().parse(new ByteArrayInputStream(source)); NodeList nodes = document.getElementsByTagNameNS( "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties", "property"); Map result = new LinkedHashMap<>(); for (int index = 0; index < nodes.getLength(); index++) { Element property = (Element) nodes.item(index); String name = property.getAttribute("name"); if (NEW_FLOW_VERSION_PROPERTY.equals(name) || EDIT_FLOW_VERSION_PROPERTY.equals(name)) { // The editor may temporarily append a newer value. The final occurrence is authoritative. result.put(name, property.getTextContent().trim()); } } return result; } }