Wstęp
Kiedy zespoły prawne lub analitycy kryminalistyczni muszą udowodnić, że dokument nie został podmieniony, samo patrzenie na widoczną treść nie wystarcza. Ukryte właściwości — takie jak autor, data utworzenia czy numer wersji — mogą ujawnić, kto i kiedy dotknął 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 dla Javy zapewnia programistyczny sposób na wyodrębnienie każdego pola metadanych i obliczenie ustrukturyzowanej różnicy pomiędzy dwoma wersjami. W tym samouczku porównamy trzy praktyczne podejścia: pełną różnicę metadanych, 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 problemem, przeglądając umowę, którą edytowało wiele stron przez kilka 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 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 dla Javy 24.7 (tymczasowa licencja)
- Dwa pliki dokumentów, które chcesz porównać (np.
contract_v1.pdficontract_v2.pdf)
Instalacja
Dodaj zależność w Mavenie:
<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: Zbiera wszystkie tagi, w tym niestandardowe.
- Prosta logika map: Nie wymaga zewnętrznej biblioteki diff.
- Obiekt wyniku: Mapy
added,removedichangedsą gotowe do dalszego przetwarzania.
💡 Wskazówka: 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
[old, new]. - Gotowe do zgodności: Idealne dla e‑discovery lub sporów o własność umowy.
💡 Wskazówka: 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 w historii wersji
Numery wersji, znaczniki czasu edycji i daty drukowania są niewidoczne dla użytkownika, a jednak 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;
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;
Kluczowe punkty:
- Odtwarzanie linii czasu: Pokazuje, ile razy plik został edytowany lub wydrukowany.
- Różnice liczbowe: Przydatne do wykrywania podejrzanie szybkich wersji.
- Lekka: Zapytanie obejmuje tylko trzy tagi, co zapewnia szybkie wykonanie.
💡 Wskazówka: Użyj tej metody, gdy musisz udowodnić, że dokument nie został zmieniony po określonym terminie.
Porównanie metod: kiedy używać której
| Metoda | Najlepsze zastosowanie | Kluczowe zalety | Ograniczenia |
|---|---|---|---|
| Pełna różnica metadanych | Pełny audyt, zgodność regulacyjna | Zbiera wszystkie właściwości, w tym niestandardowe tagi | Większe zużycie pamięci przy bardzo dużych plikach |
| Wykrywanie zmian własności | Spory prawne o własność, e‑discovery | Skupia się na polach związanych z osobą, łatwe do odczytania | Ignoruje inne przydatne metadane |
| Wykrywanie zmian w historii wersji | Forensyka chronologiczna, weryfikacja dziennika 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 kombinacja — uruchomienie pełnego diffu, a następnie zagłębienie się w sekcje własności lub historii — zapewnia najwięcej informacji.
Eksportowanie różnicy
Po uzyskaniu obiektu MetadataDiff często trzeba podzielić się wynikami. Poniżej dwa proste eksportery.
Eksport CSV
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));
Eksport JSON
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));
Oba eksportery korzystają z metod pomocniczych (esc, escape, writeMap), które bezpiecznie obsługują przecinki i cudzysłowy.
Najlepsze praktyki i wskazówki
- Ogranicz zakres diffu: W przypadku dużych PDF‑ów ogranicz diff do tagów własności lub historii, aby skrócić czas przetwarzania.
- Waliduj format pliku: Zawsze sprawdzaj
metadata.getFileFormat() != FileFormat.Unknownprzed iteracją po właściwościach. - Zwalniaj zasoby: Używaj try‑with‑resources (
try (Metadata metadata = new Metadata(path)) { … }), aby zwolnić natywne uchwyty. - Spójność wersji: Upewnij się, że oba dokumenty pochodzą z tej samej wersji formatu; mieszanie DOCX i starszego DOC może dawać mylące wyniki.
- Bezpieczeństwo: Nigdy nie udostępniaj surowych wartości metadanych w publicznych API bez sanitizacji; używaj funkcji
esc/escapeprzy zapisie CSV/JSON. - Wydajność: Eksportuj do CSV przy masowym wprowadzaniu do SIEM‑ów; JSON jest lepszy dla czytelnych dla człowieka logów audytowych.
Zakończenie
GroupDocs.Metadata dla Javy upraszcza forensykę metadanych. Wykorzystując pełny diff, wykrywanie zmian własności oraz analizę historii wersji, możesz zbudować solidny potok audytowy, który ujawnia ukryte zmiany, wspiera dowody prawne i spełnia wymogi zgodności. Eksport do CSV lub JSON umożliwia płynną integrację z narzędziami raportującymi lub hurtowniami danych.
Kolejne kroki:
- Zapoznaj się z zaawansowanymi specyfikacjami tagów, aby filtrować niestandardowe metadane GroupDocs.Metadata Java docs.
- Dowiedz się, jak porównywać wiele dokumentów w partii API reference.
- Przejrzyj oficjalne przykłady projektów end‑to‑end GitHub examples.