有没有办法使用 Apache POI 为 docx 文件设置固定元数据?
Is there a way to set fixed metadata for docx files by using Apache POI?
我想准备一些用于测试的参考docx文件。此文件包含一组按特定顺序排列的字符串。
有一些 REST API,通过调用它们我将文件作为字节数组获取,在我的测试中我想将它们与参考文件进行比较。
要生成 docx 文件,我使用 Apache POI 库。例如:
...
XWPFDocument document = new XWPFDocument();
XWPFParagraph title = document.createParagraph();
title.setAlignment(ParagraphAlignment.LEFT);
XWPFRun titleRun = title.createRun();
titleRun.setFontFamily("Arial");
titleRun.setFontSize(11);
for (int i = 0; i < fileNames.size(); i++) {
titleRun.setText(format("%d. %s", (i + 1), fileNames.get(i)));
titleRun.addBreak();
}
...
这里我需要设置一个固定的元数据。我是这样做的:
@SneakyThrows
private void clearDocxMetadata(XWPFDocument document) {
CoreProperties props = document.getProperties().getCoreProperties();
props.setCreated("2019-08-14T21:00:00z");
props.setLastModifiedByUser(StringUtils.EMPTY);
props.setCreator(StringUtils.EMPTY);
props.setLastPrinted("2019-08-14T21:00:00z");
props.setModified("2019-08-14T21:00:00z");
document.getProperties().commit();
}
REST API 使用相同的代码生成 docx 文件,我相信元数据会被冻结。
但是,生成的文件有时会发生变化,字节数组相等性测试给出以下结果:
org.opentest4j.AssertionFailedError: array contents differ at index [10], expected: <-3> but was: <98>
org.opentest4j.AssertionFailedError: array contents differ at index [10], expected: <-3> but was: <97>
文件内容相同:
但在十六进制模式下我看到了不同之处:
从 \docProps:
中解压 core.xml 个引用的 docx 文件
<?xml version="1.0" encoding="UTF-8"?>
<cp:coreProperties
xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2019-08-14T00:00:00Z</dcterms:created>
<dc:creator>2019-08-14T21:00:00z</dc:creator>
<cp:lastModifiedBy>2019-08-14T21:00:00z</cp:lastModifiedBy>
<cp:lastPrinted>2019-08-14T00:00:00Z</cp:lastPrinted>
<dcterms:modified xsi:type="dcterms:W3CDTF">2019-08-14T00:00:00Z</dcterms:modified>
</cp:coreProperties>
某些元数据似乎正在发生变化(很可能是日期)。
如果我在测试代码中设置元数据,它也没有效果:
@SneakyThrows
private void setCustomDocxMetadata(InputStream inputStream, Date date) {
try (OPCPackage opc = OPCPackage.open(inputStream)) {
PackageProperties docxMetadata = opc.getPackageProperties();
docxMetadata.setModifiedProperty(Optional.of(date));
docxMetadata.setCreatedProperty(Optional.of(date));
docxMetadata.setLastModifiedByProperty(StringUtils.EMPTY);
docxMetadata.setCreatorProperty(StringUtils.EMPTY);
}
}
有没有办法使用 Apache POI 为 docx 文件设置固定元数据?
您发现的差异是 *.docx
ZIP
存档中条目的最后修改文件日期和时间。这与您已经设置的文件属性无关。
根据 ZIP file format 这正是您在十六进制转储中标记的字节。条目从 0 开始,有 4 个字节 504B0304
,在偏移量 10 处有 2 个字节表示最后修改时间,在偏移量 12 处有 2 个字节表示最后修改日期。
*.docx
ZIP
存档中条目的修改文件日期和时间是在写出 XWPFDocument
和 *.docx
[=14= 时设置的] 包含条目的存档被创建。没有正确的方法进入这个过程。
我找到的唯一方法是,在写完文档后从数据中创建一个临时 ZIP
文件。然后使用 java.util.zip.*
来处理 *.docx
ZIP
存档中所有条目的最后修改文件日期和时间。
代码:
import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.ooxml.POIXMLProperties.CoreProperties;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.zip.*;
public class CreateXWPFFixedZIPCreationDateTime {
static byte[] createXWPFZIPArchive() throws Exception {
XWPFDocument document = new XWPFDocument();
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run=paragraph.createRun();
run.setText("The content");
clearDocxMetadata(document);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
document.write(byteOut);
byteOut.flush();
byte[] zipData = byteOut.toByteArray();
zipData = clearZIPEntryLastModified(zipData);
return zipData;
}
static byte[] clearZIPEntryLastModified(byte[] zipData) throws Exception {
File tmpZipFile = File.createTempFile("zip", ".tmp");
tmpZipFile.deleteOnExit();
FileOutputStream fileOut = new FileOutputStream(tmpZipFile);
fileOut.write(zipData);
fileOut.close();
ZipFile zipFile = new ZipFile(tmpZipFile);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(byteOut);
for(Enumeration enumeration = zipFile.entries(); enumeration.hasMoreElements(); ) {
ZipEntry entry = (ZipEntry) enumeration.nextElement();
entry.setTime(new GregorianCalendar(2019,7,14,21,0,0).getTime().getTime());
zipOut.putNextEntry(entry);
InputStream is = zipFile.getInputStream(entry);
byte[] buf = new byte[1024]; int len;
while ((len = (is.read(buf))) > 0) {
zipOut.write(buf, 0, (len < buf.length) ? len : buf.length);
}
zipOut.closeEntry();
}
zipFile.close();
zipOut.close();
byteOut.flush();
return byteOut.toByteArray();
}
static void clearDocxMetadata(XWPFDocument document) throws Exception {
CoreProperties props = document.getProperties().getCoreProperties();
props.setCreated("2019-08-14T21:00:00z");
props.setLastModifiedByUser("");
props.setCreator("");
props.setLastPrinted("2019-08-14T21:00:00z");
props.setModified("2019-08-14T21:00:00z");
document.getProperties().commit();
}
public static void main(String[] args) throws Exception {
byte[] bytes1 = createXWPFZIPArchive();
Thread.sleep(5000);
byte[] bytes2 = createXWPFZIPArchive();
for (int i = 0; i < bytes1.length; i++) {
byte b1 = bytes1[i];
byte b2 = 0;
if (i < bytes2.length) b2 = bytes2[i];
String sb1 = String.format("%02x", b1);
String sb2 = String.format("%02x", b2);
String att = "";if (b1 != b2) att="!";
if (i == 0 || i % 8 != 0) {
System.out.print(att+sb1+":"+sb2+"\t");
} else {
System.out.println();
System.out.print(att+sb1+":"+sb2+"\t");
}
}
System.out.println();
FileOutputStream fileOut = new FileOutputStream("Word1.docx");
fileOut.write(bytes1);
fileOut.close();
fileOut = new FileOutputStream("Word2.docx");
fileOut.write(bytes2);
fileOut.close();
}
}
如果注释掉代码行:
zipData = clearZIPEntryLastModified(zipData);
到
//zipData = clearZIPEntryLastModified(zipData);
您会发现输出与 *.docx
ZIP
存档中所有条目的最后修改文件日期和时间的字节数完全不同。
我想准备一些用于测试的参考docx文件。此文件包含一组按特定顺序排列的字符串。
有一些 REST API,通过调用它们我将文件作为字节数组获取,在我的测试中我想将它们与参考文件进行比较。
要生成 docx 文件,我使用 Apache POI 库。例如:
...
XWPFDocument document = new XWPFDocument();
XWPFParagraph title = document.createParagraph();
title.setAlignment(ParagraphAlignment.LEFT);
XWPFRun titleRun = title.createRun();
titleRun.setFontFamily("Arial");
titleRun.setFontSize(11);
for (int i = 0; i < fileNames.size(); i++) {
titleRun.setText(format("%d. %s", (i + 1), fileNames.get(i)));
titleRun.addBreak();
}
...
这里我需要设置一个固定的元数据。我是这样做的:
@SneakyThrows
private void clearDocxMetadata(XWPFDocument document) {
CoreProperties props = document.getProperties().getCoreProperties();
props.setCreated("2019-08-14T21:00:00z");
props.setLastModifiedByUser(StringUtils.EMPTY);
props.setCreator(StringUtils.EMPTY);
props.setLastPrinted("2019-08-14T21:00:00z");
props.setModified("2019-08-14T21:00:00z");
document.getProperties().commit();
}
REST API 使用相同的代码生成 docx 文件,我相信元数据会被冻结。
但是,生成的文件有时会发生变化,字节数组相等性测试给出以下结果:
org.opentest4j.AssertionFailedError: array contents differ at index [10], expected: <-3> but was: <98>
org.opentest4j.AssertionFailedError: array contents differ at index [10], expected: <-3> but was: <97>
文件内容相同:
但在十六进制模式下我看到了不同之处:
从 \docProps:
中解压 core.xml 个引用的 docx 文件<?xml version="1.0" encoding="UTF-8"?>
<cp:coreProperties
xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2019-08-14T00:00:00Z</dcterms:created>
<dc:creator>2019-08-14T21:00:00z</dc:creator>
<cp:lastModifiedBy>2019-08-14T21:00:00z</cp:lastModifiedBy>
<cp:lastPrinted>2019-08-14T00:00:00Z</cp:lastPrinted>
<dcterms:modified xsi:type="dcterms:W3CDTF">2019-08-14T00:00:00Z</dcterms:modified>
</cp:coreProperties>
某些元数据似乎正在发生变化(很可能是日期)。
如果我在测试代码中设置元数据,它也没有效果:
@SneakyThrows
private void setCustomDocxMetadata(InputStream inputStream, Date date) {
try (OPCPackage opc = OPCPackage.open(inputStream)) {
PackageProperties docxMetadata = opc.getPackageProperties();
docxMetadata.setModifiedProperty(Optional.of(date));
docxMetadata.setCreatedProperty(Optional.of(date));
docxMetadata.setLastModifiedByProperty(StringUtils.EMPTY);
docxMetadata.setCreatorProperty(StringUtils.EMPTY);
}
}
有没有办法使用 Apache POI 为 docx 文件设置固定元数据?
您发现的差异是 *.docx
ZIP
存档中条目的最后修改文件日期和时间。这与您已经设置的文件属性无关。
根据 ZIP file format 这正是您在十六进制转储中标记的字节。条目从 0 开始,有 4 个字节 504B0304
,在偏移量 10 处有 2 个字节表示最后修改时间,在偏移量 12 处有 2 个字节表示最后修改日期。
*.docx
ZIP
存档中条目的修改文件日期和时间是在写出 XWPFDocument
和 *.docx
[=14= 时设置的] 包含条目的存档被创建。没有正确的方法进入这个过程。
我找到的唯一方法是,在写完文档后从数据中创建一个临时 ZIP
文件。然后使用 java.util.zip.*
来处理 *.docx
ZIP
存档中所有条目的最后修改文件日期和时间。
代码:
import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.ooxml.POIXMLProperties.CoreProperties;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.zip.*;
public class CreateXWPFFixedZIPCreationDateTime {
static byte[] createXWPFZIPArchive() throws Exception {
XWPFDocument document = new XWPFDocument();
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run=paragraph.createRun();
run.setText("The content");
clearDocxMetadata(document);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
document.write(byteOut);
byteOut.flush();
byte[] zipData = byteOut.toByteArray();
zipData = clearZIPEntryLastModified(zipData);
return zipData;
}
static byte[] clearZIPEntryLastModified(byte[] zipData) throws Exception {
File tmpZipFile = File.createTempFile("zip", ".tmp");
tmpZipFile.deleteOnExit();
FileOutputStream fileOut = new FileOutputStream(tmpZipFile);
fileOut.write(zipData);
fileOut.close();
ZipFile zipFile = new ZipFile(tmpZipFile);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(byteOut);
for(Enumeration enumeration = zipFile.entries(); enumeration.hasMoreElements(); ) {
ZipEntry entry = (ZipEntry) enumeration.nextElement();
entry.setTime(new GregorianCalendar(2019,7,14,21,0,0).getTime().getTime());
zipOut.putNextEntry(entry);
InputStream is = zipFile.getInputStream(entry);
byte[] buf = new byte[1024]; int len;
while ((len = (is.read(buf))) > 0) {
zipOut.write(buf, 0, (len < buf.length) ? len : buf.length);
}
zipOut.closeEntry();
}
zipFile.close();
zipOut.close();
byteOut.flush();
return byteOut.toByteArray();
}
static void clearDocxMetadata(XWPFDocument document) throws Exception {
CoreProperties props = document.getProperties().getCoreProperties();
props.setCreated("2019-08-14T21:00:00z");
props.setLastModifiedByUser("");
props.setCreator("");
props.setLastPrinted("2019-08-14T21:00:00z");
props.setModified("2019-08-14T21:00:00z");
document.getProperties().commit();
}
public static void main(String[] args) throws Exception {
byte[] bytes1 = createXWPFZIPArchive();
Thread.sleep(5000);
byte[] bytes2 = createXWPFZIPArchive();
for (int i = 0; i < bytes1.length; i++) {
byte b1 = bytes1[i];
byte b2 = 0;
if (i < bytes2.length) b2 = bytes2[i];
String sb1 = String.format("%02x", b1);
String sb2 = String.format("%02x", b2);
String att = "";if (b1 != b2) att="!";
if (i == 0 || i % 8 != 0) {
System.out.print(att+sb1+":"+sb2+"\t");
} else {
System.out.println();
System.out.print(att+sb1+":"+sb2+"\t");
}
}
System.out.println();
FileOutputStream fileOut = new FileOutputStream("Word1.docx");
fileOut.write(bytes1);
fileOut.close();
fileOut = new FileOutputStream("Word2.docx");
fileOut.write(bytes2);
fileOut.close();
}
}
如果注释掉代码行:
zipData = clearZIPEntryLastModified(zipData);
到
//zipData = clearZIPEntryLastModified(zipData);
您会发现输出与 *.docx
ZIP
存档中所有条目的最后修改文件日期和时间的字节数完全不同。