Introduction

When legal teams or forensic analysts need to prove that a document has not been tampered with, simply looking at the visible content is not enough. Hidden properties—such as author, creation date, or revision number—can reveal who touched a file and when. Detecting these subtle changes across document versions is a common pain point that often requires manual inspection of each property, a time‑consuming and error‑prone task.

GroupDocs.Metadata for Java provides a programmatic way to extract every metadata field and compute a structured diff between two versions. In this tutorial we will compare three practical approaches: a full metadata diff, focused ownership change detection, and revision‑history analysis. Each method is demonstrated with concise, copy‑pasteable code and we will also show how to export the results to CSV or JSON for audit reporting.

I hit this when reviewing a contract that had been edited by multiple parties over months; the visible text looked identical, but the ownership fields had changed silently.

How can I tell which metadata fields changed between two document versions?

GroupDocs.Metadata loads each file, extracts all accessible properties into a map, and then iterates over the keys to classify additions, removals, and modifications. The library handles built‑in and custom tags, so you get a complete picture without writing format‑specific parsers. The result is a MetadataDiff object that you can query or serialize for compliance reports.

Prerequisites

  • Java 8 or later
  • GroupDocs.Metadata for Java 24.7 (temporary license)
  • Two document files you want to compare (e.g., contract_v1.pdf and contract_v2.pdf)

Installation

Add the dependency via Maven:

<dependency>
    <groupId>com.groupdocs</groupId>
    <artifactId>groupdocs-metadata</artifactId>
    <version>24.7</version>
</dependency>

Method 1 – Full Metadata Diff

This method extracts every metadata property from both versions and reports added, removed, and changed entries.

// CompareMetadataSets.run – returns a MetadataDiff object
Map<String, String> v1 = ExtractAllMetadata.run(pathV1);
Map<String, String> v2 = ExtractAllMetadata.run(pathV2);
MetadataDiff diff = new MetadataDiff();

// Detect added and changed properties
for (Map.Entry<String, String> e : v2.entrySet()) {
    String key = e.getKey();
    String val = e.getValue();
    if (!v1.containsKey(key)) {
        diff.added.put(key, val);               // New property in v2
    } else if (!v1.get(key).equals(val)) {
        diff.changed.put(key, new String[]{v1.get(key), val}); // Value changed
    }
}
// Detect removed properties
for (Map.Entry<String, String> e : v1.entrySet()) {
    if (!v2.containsKey(e.getKey())) {
        diff.removed.put(e.getKey(), e.getValue());
    }
}
return diff;

Key points:

  • Comprehensive: Captures all tags, including custom ones.
  • Simple map logic: No external diff library required.
  • Result object: added, removed, and changed maps are ready for further processing.

💡 Tip: Use this when you need a full audit trail for regulatory compliance.

Method 2 – Detect Ownership Changes

Legal disputes often hinge on who created or edited a document. This method focuses on person‑related tags such as Creator, Editor, Manager, and Company.

// DetectOwnershipChanges.run – returns a map of changed ownership fields
Map<String, String> v1 = readOwnership(pathV1);
Map<String, String> v2 = readOwnership(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
    String oldVal = v1.getOrDefault(key, "<missing>");
    String newVal = v2.getOrDefault(key, "<missing>");
    if (!oldVal.equals(newVal)) {
        changes.put(key, new String[]{oldVal, newVal});
    }
}
return changes;

readOwnership pulls only the relevant tags:

Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return result;
    for (MetadataProperty p : metadata.findProperties(
            new ContainsTagSpecification(Tags.getPerson().getCreator())
                .or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
                .or(new ContainsTagSpecification(Tags.getPerson().getManager()))
                .or(new ContainsTagSpecification(Tags.getCorporate().getCompany())))) {
        String value = "";
        if (p.getValue() != null && p.getValue().getRawValue() != null) {
            value = String.valueOf(p.getValue().getRawValue());
        }
        result.put(p.getName(), value);
    }
}
return result;

Key points:

  • Targeted: Only identity‑bearing properties are examined.
  • Clear output: Returns a map where each entry shows [old, new] values.
  • Compliance‑ready: Perfect for e‑discovery or contract‑ownership disputes.

💡 Tip: Combine this with the full diff if you need both breadth and depth.

Method 3 – Detect Revision History Changes

Revision numbers, edit timestamps, and print dates are invisible to end users but crucial for forensic timelines. This method isolates time‑related tags.

Map<String, String> v1 = readRevision(pathV1);
Map<String, String> v2 = readRevision(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
    String oldVal = v1.getOrDefault(key, "<missing>");
    String newVal = v2.getOrDefault(key, "<missing>");
    if (!oldVal.equals(newVal)) {
        changes.put(key, new String[]{oldVal, newVal});
    }
}
return changes;

readRevision extracts Modified, Created, and Printed timestamps:

Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return result;
    for (MetadataProperty p : metadata.findProperties(
            new ContainsTagSpecification(Tags.getTime().getModified())
                .or(new ContainsTagSpecification(Tags.getTime().getCreated()))
                .or(new ContainsTagSpecification(Tags.getTime().getPrinted())))) {
        String value = "";
        if (p.getValue() != null && p.getValue().getRawValue() != null) {
            value = String.valueOf(p.getValue().getRawValue());
        }
        result.put(p.getName(), value);
    }
}
return result;

Key points:

  • Timeline reconstruction: Shows how many times a file was edited or printed.
  • Numeric deltas: Useful for detecting suspicious rapid revisions.
  • Lightweight: Only three tags are queried, keeping execution fast.

💡 Tip: Use this when you need to prove a document was not altered after a specific deadline.

Comparing Methods: When to Use Each

Method Best For Key Advantages Limitations
Full Metadata Diff Full audit, regulatory compliance Captures every property, including custom tags Larger memory footprint for very big files
Ownership Change Detection Legal ownership disputes, e‑discovery Focuses on person‑related fields, easy to read Ignores other useful metadata
Revision History Detection Timeline forensics, change‑log verification Isolates timestamps and revision numbers Does not show content‑level changes

Choose the method that aligns with your compliance goal. In many cases a combination—run the full diff and then drill into ownership or revision sections—provides the most insight.

Exporting the Diff

After obtaining a MetadataDiff, you often need to share the findings. Below are two simple exporters.

CSV Export

StringBuilder sb = new StringBuilder();
sb.append("change_type,property,old_value,new_value\n");
for (Map.Entry<String, String> e : diff.added.entrySet()) {
    sb.append("added,").append(esc(e.getKey()))
      .append(",,").append(esc(e.getValue())).append("\n");
}
for (Map.Entry<String, String> e : diff.removed.entrySet()) {
    sb.append("removed,").append(esc(e.getKey()))
      .append(",").append(esc(e.getValue()))
      .append(",\n");
}
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
    sb.append("changed,").append(esc(e.getKey()))
      .append(",").append(esc(e.getValue()[0]))
      .append(",").append(esc(e.getValue()[1])).append("\n");
}
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));

JSON Export

StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append("  \"added\": {\n");
writeMap(sb, diff.added);
sb.append("  },\n");
sb.append("  \"removed\": {\n");
writeMap(sb, diff.removed);
sb.append("  },\n");
sb.append("  \"changed\": {\n");
int i = 0;
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
    String comma = ++i < diff.changed.size() ? "," : "";
    sb.append("    \"").append(escape(e.getKey()))
      .append("\": { \"from\": \"")
      .append(escape(e.getValue()[0])).append("\", \"to\": \"")
      .append(escape(e.getValue()[1])).append("\" }")
      .append(comma).append("\n");
}
sb.append("  }\n");
sb.append("}\n");
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));

Both exporters rely on helper methods (esc, escape, writeMap) that safely handle commas and quotation marks.

Best Practices and Tips

  • Scope your diff: For large PDFs, limit the diff to ownership or revision tags to reduce processing time.
  • Validate file format: Always check metadata.getFileFormat() != FileFormat.Unknown before iterating properties.
  • Dispose resources: Use try‑with‑resources (try (Metadata metadata = new Metadata(path)) { … }) to free native handles.
  • Version consistency: Ensure both documents are from the same file format version; mixing DOCX and older DOC can yield misleading results.
  • Security: Never expose raw metadata values in public APIs without sanitization; use the esc/escape helpers when writing CSV/JSON.
  • Performance: Export to CSV for bulk ingestion into SIEMs; JSON is better for human‑readable audit logs.

Conclusion

GroupDocs.Metadata for Java makes metadata forensics straightforward. By leveraging the full diff, ownership detection, and revision‑history analysis methods, you can build a robust audit pipeline that surfaces hidden changes, supports legal evidence, and satisfies compliance requirements. Exporting to CSV or JSON enables seamless integration with reporting tools or data‑warehouse pipelines.

Next steps:

Additional Resources