package jnpf.bizcommon.onlyoffice.service.impl; import jnpf.exception.DataException; import org.springframework.stereotype.Component; 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 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.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; /** Canonicalizes qianpiye flow-version properties after DocumentServer saves a DOCX. */ @Component public class DmsQianpiyeDocxPropertiesWriter { static final String CUSTOM_PROPERTIES_PART = "docProps/custom.xml"; static final String NEW_FLOW_VERSION_PROPERTY = "DMS_NEW_FLOW_VERSION_ID"; static final String EDIT_FLOW_VERSION_PROPERTY = "DMS_EDIT_FLOW_VERSION_ID"; private static final String CUSTOM_PROPERTIES_NAMESPACE = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"; private static final String DOC_PROPERTIES_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; private static final String FORMAT_ID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; /** Keeps non-qianpiye and legacy documents intact; duplicate staged values use the last one. */ public byte[] upsertStagedFlowVersions(byte[] source) { if (source == null || source.length == 0) return source; try { Map parts = readParts(source); byte[] custom = parts.get(CUSTOM_PROPERTIES_PART); if (custom == null) return source; Map versions = readFlowVersions(custom); if (!StringUtils.hasText(versions.get(NEW_FLOW_VERSION_PROPERTY)) || !StringUtils.hasText(versions.get(EDIT_FLOW_VERSION_PROPERTY))) return source; parts.put(CUSTOM_PROPERTIES_PART, writeFlowVersions(custom, versions)); return writeParts(parts); } catch (DataException e) { throw e; } catch (Exception e) { throw new DataException("写入签批页流程版本属性失败: " + e.getMessage()); } } private Map readParts(byte[] source) throws Exception { Map result = new LinkedHashMap<>(); try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(source))) { ZipEntry entry; while ((entry = input.getNextEntry()) != null) { if (entry.isDirectory()) continue; ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; for (int read; (read = input.read(buffer)) >= 0; ) output.write(buffer, 0, read); result.put(entry.getName(), output.toByteArray()); } } return result; } private byte[] writeParts(Map parts) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); try (ZipOutputStream zip = new ZipOutputStream(output)) { for (Map.Entry part : parts.entrySet()) { zip.putNextEntry(new ZipEntry(part.getKey())); zip.write(part.getValue()); zip.closeEntry(); } } return output.toByteArray(); } private Map readFlowVersions(byte[] source) throws Exception { org.w3c.dom.Document document = parse(source); NodeList properties = document.getElementsByTagNameNS(CUSTOM_PROPERTIES_NAMESPACE, "property"); Map result = new LinkedHashMap<>(); for (int index = 0; index < properties.getLength(); index++) { Element property = (Element) properties.item(index); String name = property.getAttribute("name"); if (NEW_FLOW_VERSION_PROPERTY.equals(name) || EDIT_FLOW_VERSION_PROPERTY.equals(name)) { result.put(name, property.getTextContent().trim()); } } return result; } private byte[] writeFlowVersions(byte[] source, Map versions) throws Exception { org.w3c.dom.Document document = parse(source); Element root = document.getDocumentElement(); NodeList properties = document.getElementsByTagNameNS(CUSTOM_PROPERTIES_NAMESPACE, "property"); int maxPid = 1; for (int index = properties.getLength() - 1; index >= 0; index--) { Element property = (Element) properties.item(index); try { maxPid = Math.max(maxPid, Integer.parseInt(property.getAttribute("pid"))); } catch (NumberFormatException ignored) { } String name = property.getAttribute("name"); if (NEW_FLOW_VERSION_PROPERTY.equals(name) || EDIT_FLOW_VERSION_PROPERTY.equals(name)) root.removeChild(property); } appendProperty(document, root, maxPid + 1, NEW_FLOW_VERSION_PROPERTY, versions.get(NEW_FLOW_VERSION_PROPERTY)); appendProperty(document, root, maxPid + 2, EDIT_FLOW_VERSION_PROPERTY, versions.get(EDIT_FLOW_VERSION_PROPERTY)); TransformerFactory factory = TransformerFactory.newInstance(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); try { factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); } catch (IllegalArgumentException ignored) { } try { factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); } catch (IllegalArgumentException ignored) { } Transformer transformer = factory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); ByteArrayOutputStream output = new ByteArrayOutputStream(source.length); transformer.transform(new DOMSource(document), new StreamResult(output)); return output.toByteArray(); } private void appendProperty(org.w3c.dom.Document document, Element root, int pid, String name, String value) { Element property = document.createElementNS(CUSTOM_PROPERTIES_NAMESPACE, "property"); property.setAttribute("fmtid", FORMAT_ID); property.setAttribute("pid", String.valueOf(pid)); property.setAttribute("name", name); Element text = document.createElementNS(DOC_PROPERTIES_TYPES_NAMESPACE, "vt:lpwstr"); text.setTextContent(value); property.appendChild(text); root.appendChild(property); } private org.w3c.dom.Document 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); return factory.newDocumentBuilder().parse(new ByteArrayInputStream(source)); } }