Wprowadzenie

Kiedy zespoły prawne lub analitycy kryminalistyczni muszą udowodnić, że dokument nie został podmieniony, samo patrzenie na widoczną treść nie wystarczy. Ukryte właściwości — takie jak autor, data utworzenia czy numer wersji — mogą ujawnić, kto i kiedy dotknął pliku. Wykrywanie tych subtelnych zmian w kolejnych wersjach dokumentu jest powszechnym 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 wyodrębnienia każdego pola metadanych i obliczenia ustrukturyzowanej różnicy między dwiema wersjami. W tym samouczku porównamy trzy praktyczne podejścia: pełną różnicę metadanych, skoncentrowane wykrywanie zmian własności oraz analizę 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, przeglądając umowę, którą edytowało wiele stron przez miesiące; widoczny tekst był identyczny, ale pola własności zmieniły się po cichu.

Jak mogę określić, które pola metadanych zmieniły się między dwiema wersjami dokumentu?

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

Wymagania wstępne

  • Java 8 lub nowsza
  • GroupDocs.Metadata for Java 24.7 ([temporary license][TEMP_LICENSE_URL])
  • Dwa pliki dokumentów, które chcesz porównać (np. contract_v1.pdf i contract_v2.pdf)

Instalacja

Dodaj zależność w Maven:

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

Metoda 1 – Pełna różnica metadanych

Ta metoda wyodrębnia wszystkie właściwości metadanych z obu wersji i raportuje dodane, usunięte oraz zmienione wpisy.

// 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;

Kluczowe punkty:

  • Kompleksowo: Rejestruje wszystkie tagi, w tym niestandardowe.
  • Prosta logika map: Nie wymaga zewnętrznej biblioteki diff.
  • Obiekt wynikowy: Mapy added, removed i changed są gotowe do dalszego przetwarzania.

💡 Porada: Użyj tej metody, gdy potrzebny jest pełny ślad audytowy dla wymogów regulacyjnych.

Metoda 2 – Wykrywanie zmian własności

Spory prawne często zależą od tego, kto stworzył lub edytował dokument. Ta metoda koncentruje się na tagach związanych z osobą, takich jak Creator, Editor, Manager i 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 pobiera tylko istotne tagi:

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;

Kluczowe punkty:

  • Skoncentrowane: Badane są wyłącznie właściwości identyfikujące osobę.
  • Czytelny wynik: Zwraca mapę, w której każdy wpis pokazuje wartości [stara, nowa].
  • Gotowe do zgodności: Idealne dla e‑discovery lub sporów o własność umowy.

💡 Porada: Połącz tę metodę z pełnym diffem, jeśli potrzebujesz zarówno szerokości, jak i głębokości analizy.

Metoda 3 – Wykrywanie zmian historii wersji

Numery wersji, znaczniki czasu edycji i daty drukowania są niewidoczne dla użytkowników końcowych, ale kluczowe dla forensyki czasowej. Ta metoda izoluje tagi związane z czasem.

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 wyodrębnia znaczniki Modified, Created i Printed:

Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return result