package jnpf.service; import jnpf.exception.DataException; import org.docx4j.TraversalUtil; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.ObjectFactory; import org.docx4j.wml.SdtBlock; import org.docx4j.wml.SdtContentBlock; import org.docx4j.wml.SdtElement; import org.docx4j.wml.SdtPr; import org.docx4j.wml.Tag; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Set; /** Embeds and detects the non-printing content-control marker carried by a DMS approval page. */ @Service public class DmsApprovalPageTagService { public static final String TAG_VALUE = "DMS_APPROVAL_PAGE"; private static final ObjectFactory WML = new ObjectFactory(); public MultipartFile tagApprovalPage(MultipartFile file) { String name = filename(file); String extension = extension(name); if ("pdf".equals(extension)) { return file; } if (!"docx".equals(extension)) { throw new DataException("审批页仅支持 PDF 或 DOCX 格式"); } try { WordprocessingMLPackage word = WordprocessingMLPackage.load(file.getInputStream()); if (hasApprovalPage(word)) { return file; } List original = new ArrayList<>(word.getMainDocumentPart().getContent()); word.getMainDocumentPart().getContent().clear(); word.getMainDocumentPart().getContent().add(taggedBlock(original)); return generated(file, save(word)); } catch (DataException exception) { throw exception; } catch (Exception exception) { throw new DataException("写入审批页标签失败:" + message(exception)); } } public boolean hasApprovalPage(MultipartFile file) { if (file == null || file.isEmpty() || !"docx".equals(extension(file.getOriginalFilename()))) { return false; } try { return hasApprovalPage(WordprocessingMLPackage.load(file.getInputStream())); } catch (Exception exception) { throw new DataException("检查审批页标签失败:" + message(exception)); } } public boolean hasApprovalPage(WordprocessingMLPackage word) { if (word == null || word.getMainDocumentPart() == null) { return false; } Deque pending = new ArrayDeque<>(); pending.push(word.getMainDocumentPart().getJaxbElement()); Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); while (!pending.isEmpty()) { Object value = XmlUtils.unwrap(pending.pop()); if (value == null || !visited.add(value)) { continue; } if (value instanceof SdtElement && isApprovalPage((SdtElement) value)) { return true; } List children = TraversalUtil.getChildrenImpl(value); if (children != null) { for (Object child : children) { if (child != null) pending.push(child); } } } return false; } public SdtBlock taggedBlock(List content) { Tag tag = WML.createTag(); tag.setVal(TAG_VALUE); SdtPr properties = WML.createSdtPr(); properties.setTag(tag); SdtContentBlock taggedContent = WML.createSdtContentBlock(); if (content != null) taggedContent.getContent().addAll(content); SdtBlock block = WML.createSdtBlock(); block.setSdtPr(properties); block.setSdtContent(taggedContent); return block; } /** Removes approval pages created by this service; they are direct blocks in the main document. */ public int removeApprovalPages(WordprocessingMLPackage word) { if (word == null || word.getMainDocumentPart() == null) return 0; List content = word.getMainDocumentPart().getContent(); int before = content.size(); content.removeIf(value -> { Object unwrapped = XmlUtils.unwrap(value); return unwrapped instanceof SdtElement && isApprovalPage((SdtElement) unwrapped); }); return before - content.size(); } private boolean isApprovalPage(SdtElement element) { SdtPr properties = element.getSdtPr(); Tag tag = properties == null ? null : properties.getTag(); return tag != null && TAG_VALUE.equals(tag.getVal()); } private byte[] save(WordprocessingMLPackage word) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); word.save(output); return output.toByteArray(); } private MultipartFile generated(MultipartFile source, byte[] content) { return new GeneratedMultipartFile(source.getName(), source.getOriginalFilename(), content); } private String filename(MultipartFile file) { String name = file == null ? null : file.getOriginalFilename(); if (file == null || file.isEmpty() || name == null || name.isBlank()) { throw new DataException("审批页为空或缺少文件名"); } return name.trim(); } private String extension(String filename) { if (filename == null) return ""; int separator = filename.lastIndexOf('.'); return separator < 0 ? "" : filename.substring(separator + 1).toLowerCase(Locale.ROOT); } private String message(Exception exception) { String value = exception.getMessage(); return value == null || value.isBlank() ? exception.getClass().getSimpleName() : value; } private static final class GeneratedMultipartFile implements MultipartFile { private final String name; private final String originalFilename; private final byte[] content; private GeneratedMultipartFile(String name, String originalFilename, byte[] content) { this.name = name; this.originalFilename = originalFilename; this.content = content; } @Override public String getName() { return name; } @Override public String getOriginalFilename() { return originalFilename; } @Override public String getContentType() { return MediaType.parseMediaType( "application/vnd.openxmlformats-officedocument.wordprocessingml.document").toString(); } @Override public boolean isEmpty() { return content.length == 0; } @Override public long getSize() { return content.length; } @Override public byte[] getBytes() { return content.clone(); } @Override public InputStream getInputStream() { return new ByteArrayInputStream(content); } @Override public void transferTo(File destination) throws IOException { Files.write(destination.toPath(), content); } } }