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.pdf i 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

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.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