Introduction
Kiedy zespoły prawne lub analitycy kryminalistyczni muszą udowodnić, że dokument nie został podmieniony, samo spojrzenie na widoczną treść nie wystarczy. Ukryte właściwości — takie jak autor, data utworzenia czy numer wersji — mogą ujawnić, kto i kiedy miał dostęp do pliku. Wykrywanie tych subtelnych zmian pomiędzy wersjami dokumentu jest częstym problemem, który często wymaga ręcznej inspekcji każdej właściwości, co jest czasochłonne i podatne na błędy.
GroupDocs.Metadata for Java zapewnia programistyczny sposób na wyodrębnienie każdego pola metadanych i obliczenie strukturalnej różnicy pomiędzy dwoma wersjami. W tym samouczku porównamy trzy praktyczne podejścia: pełna różnica metadanych, wykrywanie zmian własności oraz analiza historii wersji. Każda metoda jest przedstawiona przy użyciu zwięzłego, gotowego do skopiowania kodu, a także pokażemy, jak wyeksportować wyniki do CSV lub JSON w celu raportowania audytowego.
Spotkałem się z tym problemem, przeglądając umowę, którą edytowało wiele stron w ciągu kilku miesięcy; widoczny tekst był identyczny, ale pola własności zmieniły się po cichu.
How can I tell which metadata fields changed between two document versions?
GroupDocs.Metadata ładuje każdy plik, wyodrębnia wszystkie dostępne właściwości do mapy, a następnie iteruje po kluczach, klasyfikując dodatki, usunięcia i modyfikacje. Biblioteka obsługuje wbudowane i własne tagi, więc otrzymujesz pełny obraz bez pisania parserów specyficznych dla formatu. Wynikiem jest obiekt MetadataDiff, który możesz zapytać lub zserializować do raportów zgodności.
Prerequisites
- Java 8 lub nowsza
- GroupDocs.Metadata for Java 24.7 (temporary license)
- Dwa pliki dokumentów, które chcesz porównać (np.
contract_v1.pdficontract_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
| Metoda | Najlepsze zastosowanie | Kluczowe zalety | Ograniczenia |
|---|---|---|---|
| Full Metadata Diff | Pełny audyt, zgodność regulacyjna | Rejestruje wszystkie właściwości, w tym własne tagi | Większe zużycie pamięci przy bardzo dużych plikach |
| Ownership Change Detection | Spory prawne o własność, e‑discovery | Skupia się na polach związanych z osobą, łatwe do odczytania | Ignoruje inne przydatne metadane |
| Revision History Detection | Analiza chronologii, weryfikacja logów zmian | Izoluje znaczniki czasu i numery wersji | Nie pokazuje zmian na poziomie treści |
Wybierz metodę, która najlepiej odpowiada Twoim celom zgodności. W wielu przypadkach połączenie — uruchomienie pełnej różnicy, a następnie zagłębienie się w sekcje własności lub wersji — dostarcza najwięcej informacji.
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.