package jnpf.fileStorage.dms;
|
|
import jnpf.service.DmsApprovalPageTagService;
|
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
|
import org.docx4j.wml.BooleanDefaultTrue;
|
import org.docx4j.wml.CTSdtContentRun;
|
import org.docx4j.wml.ObjectFactory;
|
import org.docx4j.wml.P;
|
import org.docx4j.wml.R;
|
import org.docx4j.wml.RPr;
|
import org.docx4j.wml.SdtPr;
|
import org.docx4j.wml.SdtRun;
|
import org.docx4j.wml.Tag;
|
import org.docx4j.wml.Text;
|
import org.docx4j.XmlUtils;
|
import org.junit.Test;
|
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.ByteArrayOutputStream;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipOutputStream;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertTrue;
|
|
public class DmsQianpiyeDocxWriterTest {
|
private static final ObjectFactory WML = new ObjectFactory();
|
private final DmsQianpiyeDocxWriter writer = new DmsQianpiyeDocxWriter();
|
|
@Test
|
public void fillsEveryDuplicateTagAndKeepsRunFormatting() throws Exception {
|
MockMultipartFile source = docx(
|
"dms.qianpiye.flow.approve-1.approver", "{{审核人}}",
|
"dms.qianpiye.flow.approve-1.approver", "{{审核人}}"
|
);
|
Map<String, String> values = Collections.singletonMap(
|
"dms.qianpiye.flow.approve-1.approver", "张三\n李四");
|
|
String xml = documentXml(writer.render(source, values));
|
|
assertEquals(2, occurrences(xml, "张三"));
|
assertEquals(2, occurrences(xml, "李四"));
|
assertEquals(2, occurrences(xml, "w:br"));
|
assertTrue(xml.contains("w:b"));
|
assertFalse(xml.contains("{{审核人}}"));
|
}
|
|
@Test
|
public void writesBlankForAValidMissingValueButLeavesInvalidRoleTagUntouched() throws Exception {
|
MockMultipartFile source = docx(
|
"dms.qianpiye.flow.approve-2.approver", "MISSING",
|
"dms.qianpiye.flow.approve-1.applicant", "KEEP"
|
);
|
|
String xml = documentXml(writer.render(source, Collections.emptyMap()));
|
|
assertFalse(xml.contains("MISSING"));
|
assertTrue(xml.contains("KEEP"));
|
}
|
|
@Test
|
public void allowsFieldControlsInsideTheWholeApprovalPageMarker() throws Exception {
|
DmsApprovalPageTagService tags = new DmsApprovalPageTagService();
|
MultipartFile tagged = tags.tagApprovalPage(docx(
|
"dms.qianpiye.form.wenjian_bianhao", "{{文件编号}}"));
|
|
String xml = documentXml(writer.render(tagged, Collections.singletonMap(
|
"dms.qianpiye.form.wenjian_bianhao", "DOC-001")));
|
|
assertTrue(xml.contains(DmsApprovalPageTagService.TAG_VALUE));
|
assertTrue(xml.contains("DOC-001"));
|
}
|
|
@Test
|
public void untaggedApprovalPageKeepsItsVisibleContentAndDoesNotBlockArchiving() throws Exception {
|
MockMultipartFile source = plainDocx("审核人");
|
|
String xml = documentXml(writer.render(source, Collections.emptyMap()));
|
|
assertTrue(xml.contains("审核人"));
|
}
|
|
@Test
|
public void resolvesTagsWhenTheTemplateContainsTheCurrentWorkflowVersion() throws Exception {
|
MockMultipartFile source = withFlowVersionProperties(docx(
|
"dms.qianpiye.flow.approve-1.approver", "{{审核人}}"), "new-flow", "revision-flow");
|
|
String xml = documentXml(writer.render(source, Collections.singletonMap(
|
"dms.qianpiye.flow.approve-1.approver", "张三"), "revision-flow"));
|
|
assertTrue(xml.contains("张三"));
|
}
|
|
@Test
|
public void fillsOlderTemplatesFromActualNewAndRevisionWorkflowValues() throws Exception {
|
MockMultipartFile source = withFlowVersionProperties(docx(
|
"dms.qianpiye.flow.approve-1.approver", "旧审核人",
|
"dms.qianpiye.form.department", "旧部门",
|
"dms.qianpiye.form.deleted_field", "已删除字段旧值",
|
"dms.qianpiye.flow.approve-9.approver", "已删除步骤旧值"), "old-new-flow", "old-revision-flow");
|
|
for (String flow : new String[]{"actual-new-flow", "actual-revision-flow"}) {
|
String xml = documentXml(writer.render(source, Map.of(
|
"dms.qianpiye.flow.approve-1.approver", flow + "审核人",
|
"dms.qianpiye.form.department", flow + "部门"), flow));
|
assertTrue(xml.contains(flow + "审核人"));
|
assertTrue(xml.contains(flow + "部门"));
|
assertFalse(xml.contains("旧审核人"));
|
assertFalse(xml.contains("旧部门"));
|
assertFalse(xml.contains("已删除字段旧值"));
|
assertFalse(xml.contains("已删除步骤旧值"));
|
}
|
}
|
|
@Test
|
public void incompleteTemplateVersionMetadataDoesNotBlockActualWorkflowRendering() throws Exception {
|
MockMultipartFile source = withFlowVersionProperties(docx(
|
"dms.qianpiye.form.department", "旧部门"), "old-new-flow", "");
|
String xml = documentXml(writer.render(source,
|
Map.of("dms.qianpiye.form.department", "当前部门"), "actual-flow"));
|
assertTrue(xml.contains("当前部门"));
|
assertFalse(xml.contains("旧部门"));
|
}
|
|
@Test
|
public void clearsMissingFieldsAndTablesIncludingPreviouslyRenderedRows() throws Exception {
|
String xml = documentXml(writer.render(docx(
|
"dms.qianpiye.form.deleted_field", "旧主表值",
|
"dms.qianpiye.form.deleted_table.column", "旧子表值",
|
"dms.qianpiye.rows.common.deleted_table", "旧表数据没有占位符"),
|
Collections.emptyMap(), "actual-flow"));
|
assertFalse(xml.contains("旧主表值"));
|
assertFalse(xml.contains("旧子表值"));
|
assertFalse(xml.contains("旧表数据没有占位符"));
|
assertTrue(xml.contains("w:b"));
|
}
|
|
@Test
|
public void rendersRepeatedRowsAsPairedValuesRatherThanIndependentColumns() throws Exception {
|
String tag = "dms.qianpiye.rows.common.distribution";
|
String xml = documentXml(writer.render(docx(tag, "{{department}}({{copies}})"),
|
Collections.singletonMap(tag, "[{\"department\":\"质量部\",\"copies\":\"10\"},"
|
+ "{\"department\":\"生产部\",\"copies\":\"20\"}]")));
|
assertTrue(xml.contains("质量部(10)"));
|
assertTrue(xml.contains("生产部(20)"));
|
assertEquals(1, occurrences(xml, "w:br"));
|
assertTrue(xml.contains("w:b"));
|
}
|
|
@Test
|
public void rejectsDeprecatedSourceBoundFieldTags() throws Exception {
|
String tag = "dms.qianpiye.v2.form-1.new_department";
|
org.junit.Assert.assertThrows(jnpf.exception.DataException.class,
|
() -> writer.render(docx(tag, "{{新部门}}"), Collections.singletonMap(tag, "质量部")));
|
}
|
|
@Test
|
public void refillsPreviouslyRenderedRowControlsUsingTheirSavedFormat() throws Exception {
|
String tag = "dms.qianpiye.rows.common.distribution";
|
MultipartFile source = docx(tag, "{{department}}({{copies}})");
|
MultipartFile first = writer.render(source, Collections.singletonMap(tag,
|
"[{\"department\":\"旧部门\",\"copies\":\"10\"}]"));
|
|
String xml = documentXml(writer.render(first, Collections.singletonMap(tag,
|
"[{\"department\":\"新部门\",\"copies\":\"20\"},"
|
+ "{\"department\":\"另一部门\",\"copies\":\"30\"}]")));
|
|
assertTrue(xml.contains("新部门(20)"));
|
assertTrue(xml.contains("另一部门(30)"));
|
assertFalse(xml.contains("旧部门"));
|
assertTrue(xml.contains("w:b"));
|
assertEquals(1, occurrences(xml, "w:br"));
|
}
|
|
@Test
|
public void emptyChildTableRendersEmptyAndMissingColumnsRenderBlank() throws Exception {
|
String tag = "dms.qianpiye.rows.common.distribution";
|
assertFalse(documentXml(writer.render(docx(tag, "{{department}}"),
|
Collections.singletonMap(tag, "[]"))).contains("{{department}}"));
|
String xml = documentXml(writer.render(docx(tag, "{{department}}({{deleted}})"),
|
Collections.singletonMap(tag, "[{\"department\":\"质量部\"}]")));
|
assertTrue(xml.contains("质量部()"));
|
assertFalse(xml.contains("{{deleted}}"));
|
}
|
|
@Test
|
public void rendersChineseRowPlaceholdersUsingExplicitAliasBindings() throws Exception {
|
String tag = "dms.qianpiye.rows.common.distribution";
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(docx(tag, "{{部门}}({{份数}})").getInputStream());
|
P paragraph = (P) word.getMainDocumentPart().getContent().get(0);
|
SdtRun control = (SdtRun) XmlUtils.unwrap(paragraph.getContent().get(0));
|
SdtPr.Alias alias = new SdtPr.Alias();
|
alias.setVal("{\"fields\":{\"部门\":\"department\",\"份数\":\"copies\"}}");
|
control.getSdtPr().getRPrOrAliasOrLock().add(alias);
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); word.save(bytes);
|
MockMultipartFile source = new MockMultipartFile("file", "approval.docx", "application/octet-stream", bytes.toByteArray());
|
String xml = documentXml(writer.render(source, Collections.singletonMap(tag,
|
"[{\"department\":\"质量部\",\"copies\":\"10\"},{\"department\":\"生产部\",\"copies\":\"20\"}]")));
|
assertTrue(xml.contains("质量部(10)"));
|
assertTrue(xml.contains("生产部(20)"));
|
}
|
|
@Test
|
public void pairsLegacyControlsWithPunctuationAndEmptyCopies() throws Exception {
|
String department = "dms.qianpiye.form.distribution.department";
|
String copies = "dms.qianpiye.form.distribution.copies";
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(docx(department, "DEPT", copies, "COPIES").getInputStream());
|
P first = (P) word.getMainDocumentPart().getContent().get(0);
|
P second = (P) word.getMainDocumentPart().getContent().remove(1);
|
first.getContent().add(textRun("("));
|
first.getContent().addAll(second.getContent());
|
first.getContent().add(textRun(")"));
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); word.save(bytes);
|
Map<String, String> values = new LinkedHashMap<>();
|
values.put(department, "运营管理部\n信息管理部"); values.put(copies, "\n20");
|
values.put("dms.qianpiye.rows.common.distribution",
|
"[{\"department\":\"运营管理部\",\"copies\":\"\"},{\"department\":\"信息管理部\",\"copies\":\"20\"}]");
|
String xml = documentXml(writer.render(new MockMultipartFile("file", "approval.docx", "application/octet-stream", bytes.toByteArray()), values));
|
javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
|
factory.setNamespaceAware(true);
|
org.w3c.dom.Document document = factory.newDocumentBuilder().parse(new java.io.ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
org.w3c.dom.NodeList paragraphs = document.getElementsByTagNameNS("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "p");
|
assertEquals(2, paragraphs.getLength());
|
assertEquals("运营管理部()", paragraphText((org.w3c.dom.Element) paragraphs.item(0)));
|
assertEquals("信息管理部(20)", paragraphText((org.w3c.dom.Element) paragraphs.item(1)));
|
assertEquals(0, occurrences(xml, "w:br"));
|
assertTrue(xml.contains("w:b"));
|
}
|
|
@Test
|
public void pairsLegacyRowsWhenAColumnWasRemovedFromTheActualWorkflow() throws Exception {
|
String department = "dms.qianpiye.form.distribution.department";
|
String deleted = "dms.qianpiye.form.distribution.deleted";
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(docx(department, "旧部门", deleted, "旧字段").getInputStream());
|
P first = (P) word.getMainDocumentPart().getContent().get(0);
|
P second = (P) word.getMainDocumentPart().getContent().remove(1);
|
first.getContent().add(textRun("("));
|
first.getContent().addAll(second.getContent());
|
first.getContent().add(textRun(")"));
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); word.save(bytes);
|
Map<String, String> values = Map.of(
|
department, "质量部\n生产部",
|
"dms.qianpiye.rows.common.distribution", "[{\"department\":\"质量部\"},{\"department\":\"生产部\"}]");
|
|
String xml = documentXml(writer.render(new MockMultipartFile("file", "approval.docx",
|
"application/octet-stream", bytes.toByteArray()), values, "actual-flow"));
|
|
assertTrue(xml.contains("质量部"));
|
assertTrue(xml.contains("生产部"));
|
assertEquals(2, occurrences(xml, "("));
|
assertEquals(2, occurrences(xml, ")"));
|
assertFalse(xml.contains("旧字段"));
|
assertFalse(xml.contains("旧部门"));
|
assertEquals(0, occurrences(xml, "w:br"));
|
}
|
|
@Test
|
public void malformedChildRowsStillFailRendering() throws Exception {
|
String tag = "dms.qianpiye.rows.common.distribution";
|
org.junit.Assert.assertThrows(jnpf.exception.DataException.class,
|
() -> writer.render(docx(tag, "{{department}}"), Map.of(tag, "[1]")));
|
org.junit.Assert.assertThrows(jnpf.exception.DataException.class,
|
() -> writer.render(docx(tag, "{{department}}"), Map.of(tag, "[{\"department\":{}}]")));
|
}
|
|
@Test
|
public void preservesBookmarkRangesAndGivesRepeatedDrawingsUniqueIds() throws Exception {
|
String xml = """
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
<w:body><w:p><w:bookmarkStart w:id="8" w:name="before"/></w:p><w:p>
|
<w:bookmarkEnd w:id="8"/><w:bookmarkStart w:id="9" w:name="pair"/>
|
<w:sdt><w:sdtPr><w:tag w:val="dms.qianpiye.form.items.department"/></w:sdtPr><w:sdtContent><w:r><w:t>D</w:t></w:r></w:sdtContent></w:sdt>
|
<w:r><w:t>(</w:t></w:r>
|
<w:sdt><w:sdtPr><w:tag w:val="dms.qianpiye.form.items.copies"/></w:sdtPr><w:sdtContent><w:r><w:t>C</w:t></w:r></w:sdtContent></w:sdt>
|
<w:r><w:t>)</w:t><w:drawing><wp:inline><wp:docPr id="1" name="image"/><pic:cNvPr id="1" name="image"/></wp:inline></w:drawing></w:r>
|
<w:bookmarkEnd w:id="9"/><w:bookmarkStart w:id="10" w:name="after"/>
|
</w:p><w:p><w:bookmarkEnd w:id="10"/></w:p></w:body>
|
</w:document>
|
""";
|
java.lang.reflect.Method render = DmsQianpiyeDocxWriter.class.getDeclaredMethod("writePart", byte[].class, Map.class, String.class);
|
render.setAccessible(true);
|
byte[] output = (byte[]) render.invoke(writer, xml.getBytes(java.nio.charset.StandardCharsets.UTF_8), Map.of(
|
"dms.qianpiye.form.items.department", "质量部\n生产部",
|
"dms.qianpiye.form.items.copies", "10\n20",
|
"dms.qianpiye.rows.common.items", "[{\"department\":\"质量部\",\"copies\":\"10\"},{\"department\":\"生产部\",\"copies\":\"20\"}]"), "word/document.xml");
|
javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
|
factory.setNamespaceAware(true);
|
org.w3c.dom.Document doc = factory.newDocumentBuilder().parse(new java.io.ByteArrayInputStream(output));
|
String w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
org.w3c.dom.NodeList paragraphs = doc.getElementsByTagNameNS(w, "p");
|
assertEquals("质量部(10)", paragraphText((org.w3c.dom.Element) paragraphs.item(1)));
|
assertEquals("生产部(20)", paragraphText((org.w3c.dom.Element) paragraphs.item(2)));
|
for (String name : new String[]{"bookmarkStart", "bookmarkEnd"}) {
|
org.w3c.dom.NodeList bookmarks = doc.getElementsByTagNameNS(w, name);
|
assertEquals(3, bookmarks.getLength());
|
java.util.Set<String> ids = new java.util.HashSet<>();
|
for (int i = 0; i < bookmarks.getLength(); i++) ids.add(((org.w3c.dom.Element) bookmarks.item(i)).getAttributeNS(w, "id"));
|
assertEquals(java.util.Set.of("8", "9", "10"), ids);
|
}
|
assertEquals(2, ((org.w3c.dom.Element) paragraphs.item(1)).getElementsByTagNameNS(w, "bookmarkStart").getLength());
|
assertEquals(0, ((org.w3c.dom.Element) paragraphs.item(2)).getElementsByTagNameNS(w, "bookmarkStart").getLength());
|
org.w3c.dom.NodeList images = doc.getElementsByTagNameNS("http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", "docPr");
|
assertEquals(2, images.getLength());
|
assertFalse(((org.w3c.dom.Element) images.item(0)).getAttribute("id").equals(((org.w3c.dom.Element) images.item(1)).getAttribute("id")));
|
}
|
|
private R textRun(String value) {
|
R run = WML.createR(); Text text = WML.createText(); text.setValue(value);
|
run.getContent().add(WML.createRT(text)); return run;
|
}
|
|
private String paragraphText(org.w3c.dom.Element paragraph) {
|
org.w3c.dom.NodeList texts = paragraph.getElementsByTagNameNS("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "t");
|
StringBuilder result = new StringBuilder();
|
for (int i = 0; i < texts.getLength(); i++) result.append(texts.item(i).getTextContent());
|
return result.toString();
|
}
|
|
private MockMultipartFile docx(String... tagsAndText) throws Exception {
|
WordprocessingMLPackage word = WordprocessingMLPackage.createPackage();
|
word.getMainDocumentPart().getContent().clear();
|
for (int index = 0; index < tagsAndText.length; index += 2) {
|
Tag tag = WML.createTag();
|
tag.setVal(tagsAndText[index]);
|
SdtPr properties = WML.createSdtPr();
|
properties.setTag(tag);
|
Text text = WML.createText();
|
text.setValue(tagsAndText[index + 1]);
|
RPr runProperties = WML.createRPr();
|
runProperties.setB(new BooleanDefaultTrue());
|
R run = WML.createR();
|
run.setRPr(runProperties);
|
run.getContent().add(WML.createRT(text));
|
CTSdtContentRun content = WML.createCTSdtContentRun();
|
content.getContent().add(run);
|
SdtRun control = WML.createSdtRun();
|
control.setSdtPr(properties);
|
control.setSdtContent(content);
|
P paragraph = WML.createP();
|
paragraph.getContent().add(WML.createPSdt(control));
|
word.getMainDocumentPart().getContent().add(paragraph);
|
}
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
word.save(output);
|
return new MockMultipartFile("file", "approval.docx",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
output.toByteArray());
|
}
|
|
private MockMultipartFile plainDocx(String text) throws Exception {
|
WordprocessingMLPackage word = WordprocessingMLPackage.createPackage();
|
word.getMainDocumentPart().getContent().clear();
|
word.getMainDocumentPart().addParagraphOfText(text);
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
word.save(output);
|
return new MockMultipartFile("file", "approval.docx",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
output.toByteArray());
|
}
|
|
private MockMultipartFile withFlowVersionProperties(MultipartFile source, String newFlow, String revisionFlow)
|
throws Exception {
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
try (ZipInputStream input = new ZipInputStream(source.getInputStream());
|
ZipOutputStream output = new ZipOutputStream(bytes)) {
|
ZipEntry entry;
|
byte[] buffer = new byte[8192];
|
while ((entry = input.getNextEntry()) != null) {
|
output.putNextEntry(new ZipEntry(entry.getName()));
|
int read;
|
while ((read = input.read(buffer)) != -1) output.write(buffer, 0, read);
|
output.closeEntry(); input.closeEntry();
|
}
|
output.putNextEntry(new ZipEntry("docProps/custom.xml"));
|
output.write(("""
|
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties\"
|
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">
|
<property name=\"DMS_NEW_FLOW_VERSION_ID\"><vt:lpwstr>%s</vt:lpwstr></property>
|
<property name=\"DMS_EDIT_FLOW_VERSION_ID\"><vt:lpwstr>%s</vt:lpwstr></property>
|
</Properties>
|
""".formatted(newFlow, revisionFlow)).getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
output.closeEntry();
|
}
|
return new MockMultipartFile("file", "approval.docx",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", bytes.toByteArray());
|
}
|
|
private String documentXml(MultipartFile file) throws Exception {
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(file.getInputStream());
|
return XmlUtils.marshaltoString(word.getMainDocumentPart().getJaxbElement(), true, true);
|
}
|
|
private int occurrences(String source, String value) {
|
int count = 0;
|
for (int index = 0; (index = source.indexOf(value, index)) >= 0; index += value.length()) count++;
|
return count;
|
}
|
}
|