package jnpf.elnService.template.docx;
|
|
import feign.Request;
|
import feign.Response;
|
import jnpf.exception.DataException;
|
import jnpf.file.FileUploadApi;
|
import jnpf.model.upload.UploadFileModel;
|
import org.junit.Test;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.nio.charset.StandardCharsets;
|
import java.lang.reflect.Proxy;
|
import java.nio.charset.Charset;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipOutputStream;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.fail;
|
|
public class DocxContentControlReaderTest {
|
|
private static final String W =
|
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
private static final String W14 =
|
"http://schemas.microsoft.com/office/word/2010/wordml";
|
private final DocxContentControlReader reader = new DocxContentControlReader();
|
|
@Test
|
public void readsBodyHeaderAndFooterWithAllSupportedControlTypes() throws Exception {
|
Map<String, String> parts = new LinkedHashMap<>();
|
parts.put("word/document.xml", part("document", "body",
|
control("eln.field.sample_code", "样品编号", "<w:text/>")
|
+ control("eln.field.remark", "备注", "<w:text w:multiLine=\"1\"/>")
|
+ control("eln.field.result", "结果", "<w:dropDownList>"
|
+ "<w:listItem w:displayText=\"合格\" w:value=\"PASS\"/>"
|
+ "</w:dropDownList>")
|
+ control("eln.field.checked", "确认", "<w14:checkbox/>")));
|
parts.put("word/header1.xml", part("hdr", null,
|
control("eln.field.test_date", "检验日期", "<w:date w:dateFormat=\"yyyy-MM-dd\"/>")));
|
parts.put("word/footer1.xml", part("ftr", null,
|
control("eln.ref.sample_code.0123456789abcdef0123456789abcdef",
|
"样品编号引用", "<w:text/>")
|
+ control("eln.field.footer_note", "页脚", "<w:text/>")));
|
|
List<DocxControlSnapshot> controls = reader.read(stream(docx(parts)));
|
|
assertEquals(7, controls.size());
|
assertControl(controls, "eln.field.sample_code", "TEXT", "PRIMARY", "word/document.xml");
|
assertControl(controls, "eln.field.remark", "MULTILINE", "PRIMARY", "word/document.xml");
|
assertControl(controls, "eln.field.result", "DROPDOWN", "PRIMARY", "word/document.xml");
|
assertControl(controls, "eln.field.checked", "CHECKBOX", "PRIMARY", "word/document.xml");
|
assertControl(controls, "eln.field.test_date", "DATE", "PRIMARY", "word/header1.xml");
|
assertControl(controls,
|
"eln.ref.sample_code.0123456789abcdef0123456789abcdef",
|
"TEXT", "REFERENCE", "word/footer1.xml");
|
DocxControlSnapshot dropdown = find(controls, "eln.field.result");
|
assertEquals("合格", dropdown.getControlConfig().get("PASS"));
|
assertEquals("value", dropdown.getCurrentValue());
|
}
|
|
@Test
|
public void readsCurrentValuesFromTextCheckboxAndDateControls() throws Exception {
|
String controls = control("eln.field.text", "文本", "<w:text/>")
|
+ "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.checked\"/><w14:checkbox>"
|
+ "<w14:checked w14:val=\"1\"/></w14:checkbox>"
|
+ "</w:sdtPr><w:sdtContent><w:r><w:t>已确认</w:t></w:r></w:sdtContent></w:sdt>"
|
+ "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.date\"/>"
|
+ "<w:date w:fullDate=\"2026-08-23T12:00:00Z\"/></w:sdtPr>"
|
+ "<w:sdtContent><w:r><w:t>2026-08-23</w:t></w:r></w:sdtContent></w:sdt>";
|
|
List<DocxControlSnapshot> result = reader.read(stream(docx(singleDocument(controls))));
|
|
assertEquals("value", find(result, "eln.field.text").getCurrentValue());
|
assertEquals("true", find(result, "eln.field.checked").getCurrentValue());
|
assertEquals("2026-08-23T12:00:00Z", find(result, "eln.field.date").getCurrentValue());
|
}
|
|
@Test
|
public void treatsDisplayedPlaceholderTextAsEmptyValue() throws Exception {
|
String xml = "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.amount\"/>"
|
+ "<w:showingPlcHdr/></w:sdtPr><w:sdtContent>"
|
+ "<w:r><w:t>请输入数值</w:t></w:r></w:sdtContent></w:sdt>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals(null, result.getCurrentValue());
|
}
|
|
@Test
|
public void preservesParagraphBreaksAndSignificantSpacesInMultilineValue() throws Exception {
|
String xml = "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.remark\"/>"
|
+ "<w:text w:multiLine=\"1\"/></w:sdtPr><w:sdtContent>"
|
+ "<w:p><w:r><w:t>第一行</w:t></w:r></w:p>"
|
+ "<w:p><w:r><w:t xml:space=\"preserve\"> 第二行 </w:t></w:r></w:p>"
|
+ "</w:sdtContent></w:sdt>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("第一行\n 第二行 ", result.getCurrentValue());
|
}
|
|
@Test
|
public void marksControlsNestedInsideAnotherContentControl() throws Exception {
|
String nested = control("eln.field.inner", "内部字段", "<w:text/>");
|
String xml = "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.outer\"/>"
|
+ "<w:text/></w:sdtPr><w:sdtContent>" + nested
|
+ "</w:sdtContent></w:sdt>";
|
|
List<DocxControlSnapshot> result = reader.read(stream(docx(singleDocument(xml))));
|
|
assertTrue(find(result, "eln.field.outer").isNestedControl());
|
assertTrue(find(result, "eln.field.inner").isNestedControl());
|
}
|
|
@Test
|
public void marksTaggedOuterControlWhenNestedControlHasNoTag() throws Exception {
|
String nested = "<w:sdt><w:sdtPr><w:text/></w:sdtPr>"
|
+ "<w:sdtContent><w:r><w:t>内部内容</w:t></w:r></w:sdtContent></w:sdt>";
|
String xml = "<w:sdt><w:sdtPr><w:tag w:val=\"eln.field.outer\"/>"
|
+ "<w:text/></w:sdtPr><w:sdtContent>" + nested
|
+ "</w:sdtContent></w:sdt>";
|
|
List<DocxControlSnapshot> result = reader.read(stream(docx(singleDocument(xml))));
|
|
assertEquals(1, result.size());
|
assertTrue(find(result, "eln.field.outer").isNestedControl());
|
}
|
|
@Test
|
public void returnsDuplicateUnknownAndRepeatControlsWithoutDroppingThem()
|
throws Exception {
|
String controls = control("eln.field.unknown_code", "未知字段", "<w:text/>")
|
+ control("eln.field.unknown_code", "未知字段副本", "<w:text/>")
|
+ control("eln.repeat.samples", "样品列表", "<w:text/>")
|
+ control("ordinary.word.control", "普通控件", "<w:text/>");
|
|
List<DocxControlSnapshot> result = reader.read(stream(docx(singleDocument(controls))));
|
|
assertEquals(3, result.size());
|
assertEquals(2, result.stream()
|
.filter(item -> "eln.field.unknown_code".equals(item.getTag())).count());
|
assertEquals("OK", find(result, "eln.repeat.samples").getStatus());
|
}
|
|
@Test
|
public void capturesRepeatPrototypeTableLocationAndUnsupportedMergeEvidence()
|
throws Exception {
|
String repeat = control("eln.repeat.samples.prototype.result", "结果", "<w:text/>");
|
String xml = "<w:tbl><w:tr><w:tc><w:tcPr><w:vMerge w:val=\"restart\"/>"
|
+ "</w:tcPr>" + repeat + "</w:tc></w:tr></w:tbl>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals(Integer.valueOf(0), result.getTableIndex());
|
assertEquals(Integer.valueOf(0), result.getTableRowIndex());
|
assertFalse(result.isNestedTable());
|
assertTrue(result.isVerticalMerge());
|
assertEquals("REPEAT", result.getControlRole());
|
}
|
|
@Test
|
public void capturesMergeEvidenceFromUnboundCellInRepeatPrototypeRow()
|
throws Exception {
|
String repeat = control("eln.repeat.samples.prototype.result", "结果", "<w:text/>");
|
String xml = "<w:tbl><w:tr><w:tc><w:tcPr><w:vMerge w:val=\"restart\"/>"
|
+ "</w:tcPr><w:p/></w:tc><w:tc>" + repeat + "</w:tc></w:tr></w:tbl>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertTrue(result.isVerticalMerge());
|
}
|
|
@Test
|
public void capturesAliasLockAndDateConfiguration() throws Exception {
|
String xml = "<w:sdt><w:sdtPr><w:alias w:val=\"采样日期\"/>"
|
+ "<w:tag w:val=\"eln.field.sample_date\"/><w:lock w:val=\"sdtLocked\"/>"
|
+ "<w:date w:dateFormat=\"yyyy-MM-dd\"/></w:sdtPr><w:sdtContent/></w:sdt>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("采样日期", result.getAlias());
|
assertEquals("sdtLocked", result.getLock());
|
assertEquals("yyyy-MM-dd", result.getControlConfig().get("dateFormat"));
|
}
|
|
@Test
|
public void treatsUntypedInlineControlCreatedByOnlyOfficeAsText() throws Exception {
|
String xml = "<w:p>" + control("eln.field.sample_code", "样品编号", "")
|
+ "</w:p>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("TEXT", result.getControlType());
|
}
|
|
@Test
|
public void treatsUntypedInlineControlNestedInParagraphMarkupAsText() throws Exception {
|
String xml = "<w:p><w:hyperlink>"
|
+ control("eln.field.sample_code", "样品编号", "")
|
+ "</w:hyperlink></w:p>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("TEXT", result.getControlType());
|
}
|
|
@Test
|
public void treatsUntypedBlockControlCreatedByOnlyOfficeAsMultiline() throws Exception {
|
String xml = control("eln.field.remark", "备注", "");
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("MULTILINE", result.getControlType());
|
}
|
|
@Test
|
public void keepsExplicitUnsupportedControlTypeUnknown() throws Exception {
|
String xml = "<w:p>" + control("eln.field.photo", "照片", "<w:picture/>")
|
+ "</w:p>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("UNKNOWN", result.getControlType());
|
}
|
|
@Test
|
public void ignoresUnsupportedTypeNamesFromForeignNamespaces() throws Exception {
|
String xml = "<w:p>" + control("eln.field.sample_code", "样品编号",
|
"<x:picture xmlns:x=\"urn:vendor-extension\"/>") + "</w:p>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("TEXT", result.getControlType());
|
}
|
|
@Test
|
public void recognizesUnsupportedTypesFromOfficialWordExtensions() throws Exception {
|
String xml = "<w:p>" + control("eln.field.repeat", "重复节",
|
"<w15:repeatingSection xmlns:w15=\"http://schemas.microsoft.com/office/word/2012/wordml\"/>")
|
+ "</w:p>";
|
|
DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0);
|
|
assertEquals("UNKNOWN", result.getControlType());
|
}
|
|
@Test
|
public void rejectsMalformedZipAndMissingDocumentPart() throws Exception {
|
expectDataException(() -> reader.read(stream("not-a-zip".getBytes(StandardCharsets.UTF_8))));
|
Map<String, String> onlyHeader = new LinkedHashMap<>();
|
onlyHeader.put("word/header1.xml", part("hdr", null, ""));
|
expectDataException(() -> reader.read(stream(docx(onlyHeader))));
|
}
|
|
@Test
|
public void rejectsDtdAndNeverResolvesExternalEntities() throws Exception {
|
String xml = "<?xml version=\"1.0\"?>"
|
+ "<!DOCTYPE w:document [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>"
|
+ "<w:document xmlns:w=\"" + W + "\"><w:body>"
|
+ control("eln.field.sample_code", "&xxe;", "<w:text/>")
|
+ "</w:body></w:document>";
|
|
expectDataException(() -> reader.read(stream(docx(singleDocumentXml(xml)))));
|
}
|
|
@Test
|
public void hidesParserImplementationDetailsForMalformedXml() throws Exception {
|
String xml = "<w:document xmlns:w=\"" + W + "\"><w:body>";
|
|
try {
|
reader.read(stream(docx(singleDocumentXml(xml))));
|
fail("预期拒绝损坏的 DOCX XML");
|
} catch (DataException expected) {
|
assertEquals("DOCX 内容控件解析失败", expected.getMessage());
|
}
|
}
|
|
@Test
|
public void rejectsPathTraversalEntries() throws Exception {
|
Map<String, String> parts = singleDocument("");
|
parts.put("word/../outside.xml", "<outside/>");
|
|
expectDataException(() -> reader.read(stream(docx(parts))));
|
}
|
|
@Test
|
public void rejectsZipBombRatio() throws Exception {
|
StringBuilder repeated = new StringBuilder(2 * 1024 * 1024);
|
for (int i = 0; i < 2 * 1024 * 1024; i++) {
|
repeated.append('A');
|
}
|
|
expectDataException(() -> reader.read(stream(docx(singleDocument(repeated.toString())))));
|
}
|
|
@Test
|
public void rejectsOversizedXmlPart() throws Exception {
|
StringBuilder oversized = new StringBuilder(10 * 1024 * 1024 + 128);
|
for (int i = 0; i < 10 * 1024 * 1024 + 128; i++) {
|
oversized.append(i % 10);
|
}
|
|
expectDataException(() -> reader.read(stream(docx(singleDocument(oversized.toString())))));
|
}
|
|
@Test
|
public void downloadsAnnexOnceAndClosesTheResponseBeforeReturningSnapshots() throws Exception {
|
byte[] bytes = docx(singleDocument(
|
control("eln.field.sample_code", "样品编号", "<w:text/>")));
|
DownloadProbe probe = new DownloadProbe(bytes);
|
FileUploadApi api = (FileUploadApi) Proxy.newProxyInstance(
|
FileUploadApi.class.getClassLoader(), new Class<?>[]{FileUploadApi.class},
|
(proxy, method, args) -> {
|
if ("downFile".equals(method.getName())) {
|
probe.model = (UploadFileModel) args[0];
|
probe.calls++;
|
return probe.response();
|
}
|
Class<?> type = method.getReturnType();
|
return type == boolean.class ? false : type == int.class ? 0 : null;
|
});
|
|
List<DocxControlSnapshot> controls =
|
new DocxContentControlReader(api).readControls("tenant-a", "template.docx");
|
|
assertEquals(1, controls.size());
|
assertEquals(1, probe.calls);
|
assertEquals(1, probe.closed);
|
assertEquals("annex", probe.model.getFolderName());
|
assertEquals("template.docx", probe.model.getObjectName());
|
}
|
|
private static void assertControl(List<DocxControlSnapshot> controls, String tag,
|
String type, String role, String part) {
|
DocxControlSnapshot control = find(controls, tag);
|
assertEquals(type, control.getControlType());
|
assertEquals(role, control.getControlRole());
|
assertEquals(part, control.getPartName());
|
assertEquals("OK", control.getStatus());
|
}
|
|
private static DocxControlSnapshot find(List<DocxControlSnapshot> controls, String tag) {
|
return controls.stream().filter(item -> tag.equals(item.getTag())).findFirst()
|
.orElseThrow(() -> new AssertionError("未找到控件: " + tag));
|
}
|
|
private static Map<String, String> singleDocument(String controls) {
|
return singleDocumentXml(part("document", "body", controls));
|
}
|
|
private static Map<String, String> singleDocumentXml(String xml) {
|
Map<String, String> parts = new LinkedHashMap<>();
|
parts.put("word/document.xml", xml);
|
return parts;
|
}
|
|
private static String part(String root, String body, String content) {
|
String bodyStart = body == null ? "" : "<w:" + body + ">";
|
String bodyEnd = body == null ? "" : "</w:" + body + ">";
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
+ "<w:" + root + " xmlns:w=\"" + W + "\" xmlns:w14=\"" + W14 + "\">"
|
+ bodyStart + content + bodyEnd + "</w:" + root + ">";
|
}
|
|
private static String control(String tag, String alias, String property) {
|
return "<w:sdt><w:sdtPr><w:alias w:val=\"" + alias + "\"/>"
|
+ "<w:tag w:val=\"" + tag + "\"/>" + property
|
+ "</w:sdtPr><w:sdtContent><w:r><w:t>value</w:t></w:r>"
|
+ "</w:sdtContent></w:sdt>";
|
}
|
|
private static byte[] docx(Map<String, String> parts) throws Exception {
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
try (ZipOutputStream zip = new ZipOutputStream(output)) {
|
for (Map.Entry<String, String> part : parts.entrySet()) {
|
zip.putNextEntry(new ZipEntry(part.getKey()));
|
zip.write(part.getValue().getBytes(StandardCharsets.UTF_8));
|
zip.closeEntry();
|
}
|
}
|
return output.toByteArray();
|
}
|
|
private static ByteArrayInputStream stream(byte[] bytes) {
|
return new ByteArrayInputStream(bytes);
|
}
|
|
private static void expectDataException(ThrowingRunnable action) {
|
try {
|
action.run();
|
fail("预期抛出 DataException");
|
} catch (DataException expected) {
|
assertFalse(expected.getMessage().trim().isEmpty());
|
} catch (Exception error) {
|
throw new AssertionError(error);
|
}
|
}
|
|
private interface ThrowingRunnable {
|
void run() throws Exception;
|
}
|
|
private static final class DownloadProbe {
|
private final byte[] bytes;
|
private UploadFileModel model;
|
private int calls;
|
private int closed;
|
|
private DownloadProbe(byte[] bytes) {
|
this.bytes = bytes;
|
}
|
|
private Response response() {
|
return Response.builder().status(200).reason("OK")
|
.request(Request.create(Request.HttpMethod.GET, "http://file/download",
|
Collections.<String, Collection<String>>emptyMap(), null,
|
Charset.defaultCharset()))
|
.body(new Response.Body() {
|
@Override public Integer length() { return bytes.length; }
|
@Override public boolean isRepeatable() { return true; }
|
@Override public java.io.InputStream asInputStream() {
|
return new ByteArrayInputStream(bytes);
|
}
|
@Override public java.io.Reader asReader(Charset charset) {
|
throw new UnsupportedOperationException();
|
}
|
@Override public void close() { closed++; }
|
}).build();
|
}
|
}
|
}
|