Вступ

Коли юридичні команди або судові аналітики мають довести, що документ не був підроблений, простий перегляд видимого вмісту недостатній. Приховані властивості — такі як автор, дата створення або номер ревізії — можуть розкрити, хто і коли торкався файлу. Виявлення цих тонких змін між версіями документів є поширеною проблемою, яка часто вимагає ручної перевірки кожної властивості, що є трудомістким і схильним до помилок процесом.

GroupDocs.Metadata for Java надає програмний спосіб витягнути всі поля метаданих і обчислити структурний diff між двома версіями. У цьому підручнику ми порівняємо три практичні підходи: повний diff метаданих, орієнтоване виявлення змін власності та аналіз історії ревізій. Кожен метод продемонстровано коротким, готовим до копіювання кодом, а також показано, як експортувати результати у CSV або JSON для аудиторської звітності.

Я зіткнувся з цим, переглядаючи контракт, який редагували кілька сторін протягом місяців; видимий текст був ідентичний, але поля власності змінилися без повідомлення.

Як визначити, які поля метаданих змінилися між двома версіями документу?

GroupDocs.Metadata завантажує кожен файл, витягує всі доступні властивості у карту і потім ітерує ключі, класифікуючи додавання, видалення та модифікації. Бібліотека обробляє вбудовані та користувацькі теги, тому ви отримуєте повну картину без написання парсерів, специфічних для формату. Результатом є об’єкт MetadataDiff, який можна запитувати або серіалізувати для звітів про відповідність.

Необхідні умови

  • Java 8 або новіше
  • GroupDocs.Metadata for Java 24.7 (temporary license)
  • Два документі, які потрібно порівняти (наприклад, contract_v1.pdf і contract_v2.pdf)

Встановлення

Додайте залежність через Maven:

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

Метод 1 – Повний diff метаданих

Цей метод витягує кожну властивість метаданих з обох версій і повідомляє про додані, видалені та змінені записи.

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

Ключові моменти:

  • Всеохоплюючий: Захоплює всі теги, включаючи користувацькі.
  • Проста логіка мапи: Не потребує зовнішньої бібліотеки diff.
  • Об’єкт результату: Карти added, removed і changed готові до подальшої обробки.

💡 Порада: Використовуйте цей метод, коли потрібен повний аудит для регуляторної відповідності.

Метод 2 – Виявлення змін власності

Юридичні спори часто залежать від того, хто створив або відредагував документ. Цей метод зосереджується на тегах, пов’язаних з особами, таких як Creator, Editor, Manager та 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 витягує лише релевантні теги:

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;

Ключові моменти:

  • Цілеспрямований: Перевіряються лише властивості, що ідентифікують особу.
  • Чіткий вихід: Повертає карту, де кожен запис показує значення [old, new].
  • Готовий до відповідності: Ідеально підходить для e‑discovery або спорів про власність контракту.

💡 Порада: Поєднайте цей метод з повним diff, якщо потрібна і ширина, і глибина аналізу.

Метод 3 – Виявлення змін історії ревізій

Номери ревізій, часові мітки редагування та дати друку невидимі користувачам, але критично важливі для судово‑медичних часових ліній. Цей метод ізолює теги, пов’язані з часом.

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 витягує часові мітки Modified, Created і 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;

Ключові моменти:

  • Відтворення часової лінії: Показує, скільки разів файл був відредагований або надрукований.
  • Числові дельти: Корисно для виявлення підозрілих швидких ревізій.
  • Легковажний: Запитуються лише три теги, що забезпечує швидке виконання.

💡 Порада: Використовуйте цей метод, коли потрібно довести, що документ не був змінений після певного дедлайну.

Порівняння методів: коли використовувати кожен

Метод Найкраще підходить для Ключові переваги Обмеження
Повний diff метаданих Повний аудит, регуляторна відповідність Захоплює всі властивості, включаючи користувацькі теги Більше споживання пам’яті для дуже великих файлів
Виявлення змін власності Юридичні спори про власність, e‑discovery Фокусується на полях, що ідентифікують особу, легко читається Ігнорує інші корисні метадані
Виявлення змін історії ревізій Судово‑медична хронологія, перевірка журналу змін Ізолює часові мітки та номери ревізій Не показує зміни на рівні вмісту

Обирайте метод, який відповідає вашій меті відповідності. У багатьох випадках комбінація — запуск повного diff і подальший аналіз розділів власності або ревізій — дає найбільшу інформативність.

Експорт diff

Після отримання MetadataDiff часто потрібно поділитися результатами. Нижче наведено два простих експортери.

Експорт у 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));

Експорт у 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));

Обидва експортери покладаються на допоміжні методи (esc, escape, writeMap), які безпечно обробляють коми та лапки.

Кращі практики та поради

  • Обмежте область diff: Для великих PDF обмежте diff лише тегами власності або ревізії, щоб скоротити час обробки.
  • Перевірка формату файлу: Завжди перевіряйте metadata.getFileFormat() != FileFormat.Unknown перед ітерацією властивостей.
  • Звільнення ресурсів: Використовуйте try‑with‑resources (try (Metadata metadata = new Metadata(path)) { … }) для звільнення нативних дескрипторів.
  • Послідовність версій: Переконайтеся, що обидва документи мають одну й ту ж версію формату; змішування DOCX і старішого DOC може дати хибні результати.
  • Безпека: Ніколи не розкривайте сирі значення метаданих у публічних API без санітизації; використовуйте допоміжні функції esc/escape при записі CSV/JSON.
  • Продуктивність: Експортуйте у CSV для масового завантаження в SIEM; JSON краще підходить для людськочитабельних аудиторських журналів.

Висновок

GroupDocs.Metadata for Java робить судово‑медичний аналіз метаданих простим. Використовуючи методи повного diff, виявлення змін власності та аналізу історії ревізій, ви можете створити надійний аудиторський конвеєр, який виявляє приховані зміни, підтримує юридичні докази та задовольняє вимоги відповідності. Експорт у CSV або JSON забезпечує безшовну інтеграцію з інструментами звітності або сховищами даних.

Наступні кроки:

  • Дослідіть розширені специфікації тегів для фільтрації користувацьких метаданих GroupDocs.Metadata Java docs.
  • Дізнайтеся, як порівнювати кілька документів у пакетному режимі API reference.
  • Перегляньте офіційні приклади проектів для повноцінних реалізацій GitHub examples.

Додаткові ресурси