有没有办法使用 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 存档中所有条目的最后修改文件日期和时间的字节数完全不同。