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.pdfandcontract_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, andchangedmaps 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.Unknownbefore 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/escapehelpers 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:
- Explore advanced tag specifications to filter custom metadata GroupDocs.Metadata Java docs.
- Learn how to compare multiple documents in a batch API reference.
- Check out the official sample projects for end‑to‑end implementations GitHub examples.